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| Parameter | Type | Required | Meaning |
|---|---|---|---|
tasks | Task[] | yes | The whole project, in any order. Read-only — the array and its elements are never mutated |
options | ScheduleOptions | no | Defaults 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;
}| Option | Type | Default | Meaning |
|---|---|---|---|
policy | SchedulingPolicy | 'off' | With 'off' the function returns immediately, before the graph is built |
calendar | WorkingCalendar | CALENDAR_DAYS | The day unit every shift, lag and duration is counted in. CALENDAR_DAYS counts all seven days |
seeds | Iterable<string> | undefined | Task ids. The walk is restricted to their downstream closure, and the seeds themselves are pinned. Omitted, every task is levelled |
hierarchy | boolean | false | true pins every id that appears as some task's parentId |
onCycle | (taskIds: string[]) => void | undefined | Called 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;
}| Field | Type | Value |
|---|---|---|
tasks | Task[] | 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 |
movedIds | string[] | Ids of the tasks the run rewrote, in the order they were reached. [] when nothing moved |
cycle | string[] | null | Every 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 |
SchedulingLink
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;
}| Field | Type | Value |
|---|---|---|
predecessorId | string | The targetId of the TaskDependency it came from |
successorId | string | The id of the task that carries the dependency |
type | DependencyType | 'FS', 'SS', 'FF' or 'SF'. What each anchors is in Dependencies |
lag | number | Days, 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); // nullConstraints
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.