Reschedule a branch#

Reschedule one task and the work that depends on it, and leave the rest of the project where it is. This is the same as gantt.autoSchedule(id, inclusive) in dhtmlxGantt.

The scope is the task you name, its summary ancestors and descendants, and its successors through links. Tasks outside the scope keep their dates.

<!- TODO: the cheatsheet says inclusive only decides whether the root task is scheduled; planner compute_scope() also walks predecessor links when inclusive. Confirm the exact scope rule. -->

Steps#

  1. Build a Subtree for the task the branch starts from. Subtree::new sets inclusive: true, so the root task is scheduled too:

    use plen_core::{SolveOptions, Subtree};
    
    let options = SolveOptions {
        subtree: Some(Subtree::new("design")),
        ..Default::default()
    };
    

    To leave the root task itself out, set inclusive: false:

    let subtree = Subtree { task: "design".into(), inclusive: false };
    
  2. Pass the options to Project::solve. Use Some(data) on the first load, or None to reschedule the current state:

    let result = project.solve(None, &options)?;
    
  3. Write the entries of result.updates back to your model. Each entry is a task in the scope that moved. The scoped dates become the accepted baseline.

The full example below has two independent chains, backend → api and design → ui. Both successors start too early. The scoped solve fixes only the design branch:

use plen_core::{
    DateTime, LinkData, LinkType, Project, ProjectData, SolveOptions, Subtree, TaskData,
};

fn task(id: &str, start: DateTime, duration: f64) -> TaskData {
    let mut t = TaskData::new(id);
    t.start_date = Some(start);
    t.duration = Some(duration);
    t
}

fn link(id: &str, source: &str, target: &str) -> LinkData {
    LinkData {
        id: id.into(),
        source: source.into(),
        target: target.into(),
        link_type: LinkType::Fs,
        lag: 0.0,
        lag_unit: None,
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let jan1 = DateTime::from_ymd_hm(2026, 1, 1, 0, 0).ok_or("invalid date")?;

    let mut data = ProjectData {
        tasks: vec![
            task("backend", jan1, 2.0),
            task("api", jan1, 1.0),
            task("design", jan1, 3.0),
            task("ui", jan1, 1.0),
        ],
        links: vec![link("l1", "backend", "api"), link("l2", "design", "ui")],
        ..Default::default()
    };
    // 24/7 time keeps the example dates easy to check.
    data.config.use_calendars = false;

    let options = SolveOptions {
        subtree: Some(Subtree::new("design")),
        ..Default::default()
    };

    let mut project = Project::default();
    let result = project.solve(Some(data), &options)?;

    for m in &result.updates {
        if let (Some(start), Some(end)) = (m.start_date, m.end_date) {
            println!("{} {}..{}", m.id, start, end);
        }
    }
    Ok(())
}

Preview a branch#

To see what rescheduling a branch would do without changing the project, set the same subtree on PreviewOptions and call Project::preview:

use plen_core::{PreviewOptions, SchedulingChanges, Subtree};

let preview = project.preview(
    &SchedulingChanges::default(),
    &PreviewOptions {
        subtree: Some(Subtree::new("backend")),
        ..Default::default()
    },
)?;

The preview scope applies to that call only. It doesn't touch the baseline or the cached solve result. A preview scope can also name a task that the same changes add.

Things to know#

  • Tasks outside the scope don't move until you solve them. In the example, the next project.solve(None, &SolveOptions::default()) moves only api, because ui is already accepted.
  • The result cache is keyed on the whole SolveOptions. A different subtree replans from the accepted dates. The same subtree again returns the cached result.
  • With include_unchanged, a scoped solve reports every task in the scope, plus the out-of-scope tasks a sparse scoped solve would report.
  • On a project with a dependency cycle, nothing is scheduled, scoped or not. Check result.cycles.
  • An unknown or unscheduled task in subtree.task fails with Error::Input at path subtree.task, message unknown or unscheduled scope task. The project keeps its previous state and cached result. See Error handling.

Result#

The example prints one line. Only ui moves, to start after design ends. api still overlaps backend:

ui 2026-01-04 00:00..2026-01-05 00:00

Without subtree, the same solve also moves api to 2026-01-03 00:00.