[Canonical page](<https://docs.dhtmlx.com/planning-engine/rust/live-projects/previewing-changes/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Previewing Changes

This guide covers how to see what an edit would do before you commit it: previewing a drag or a resize, keeping previews in sync with the project's baseline, checking the resource load a change would cause, and trying multi-step edits or a leveling run on a copy of the project.

## How previews work

A solved [`Project`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md>) holds one **accepted baseline**: the dates from its last [`solve`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#solve>) or [`level`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#level>). A preview always starts from that baseline.

[`Project::preview(&changes, &options)`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#preview>) copies the baseline, applies a [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/changes/scheduling-changes.md>) value to the copy and schedules it. The copy is dropped when the call returns. The project stays as it was, so `preview` takes `&self`, and you can call it as often as the UI needs, for example on every mouse move during a drag.

What you get back is a regular [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/results/schedule-result.md>):

- `updates` are relative to the baseline. They list the tasks the change would move and the tasks the change itself touched, like the `solve(None)` after an [`apply`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#apply>). An empty change set gives empty `updates`.
- `conflicts`, `cycles` and `notes` describe the previewed state. A preview can show a cycle that a new link would create, without the project ever holding it.
- `baseline_id` names the baseline the preview read.

A preview differs from `apply` in two places. It isn't partial: if any operation in the change set is invalid, the whole call fails with [`Error::Input`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/error.md>) for the first rejected operation. And it needs a clean, solved baseline. Before the first solve, and after any `apply` until the next `solve`, it fails with [`ProjectError::NoBaseline`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/project-error.md>).

For a change that takes more than one step, or for a `level` run, there's a second tool: [`Project::clone`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#clone>). Its data lives in shared `Arc`s, so a clone costs O(1), and the copies split only when one of them is edited. Edit, solve or level the clone. The original stays as it was.

| You want to see | Use |
|---|---|
| The effect of one change set (a drag, a resize, a new link) | `preview` |
| The load a change would put on resources | `preview` with `PreviewOptions.profile` |
| Several edits in a row, each solved | `clone`, then `apply` + `solve(None)` on the copy |
| What leveling would do | `clone`, then `level` on the copy |

## Previewing an edit

Build the edit as a [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/changes/scheduling-changes.md>), the same value you'd pass to [`apply`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#apply>), and pass it to [`preview`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#preview>).

```rust
use plen_core::{
    DateTime, LinkData, LinkType, Patch, PreviewOptions, Project, ProjectData, SchedulingChanges,
    SolveOptions, TaskData, TaskUpdate,
};

fn day(d: u32) -> Result<DateTime, Box<dyn std::error::Error>> {
    Ok(DateTime::from_ymd_hm(2026, 1, d, 0, 0).ok_or("invalid date")?)
}

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut data = ProjectData {
        tasks: vec![task("a", day(1)?, 2.0), task("b", day(1)?, 1.0)],
        links: vec![LinkData {
            id: "ab".into(),
            source: "a".into(),
            target: "b".into(),
            link_type: LinkType::Fs,
            lag: 0.0,
            lag_unit: None,
        }],
        ..Default::default()
    };
    // All time is working time: plain day arithmetic, no weekends.
    data.config.use_calendars = false;

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

    // The user drags task "b" to January 5.
    let mut drag = SchedulingChanges::default();
    let mut moved = TaskUpdate::new("b");
    moved.start_date = Patch::Value(day(5)?);
    drag.tasks.update.push(moved);

    let preview = project.preview(&drag, &PreviewOptions::default())?;
    for update in &preview.updates {
        if let (Some(start), Some(end)) = (update.start_date, update.end_date) {
            println!("task {}: {start} .. {end}", update.id);
        }
    }

    // The project still holds the solved dates: "b" starts on January 3.
    let jan5 = Some(day(5)?);
    let current = project.solve(None, &SolveOptions::default())?;
    assert!(current.updates.iter().all(|u| u.start_date != jan5));
    Ok(())
}
```

The solve puts `b` right after `a`, on January 3. The preview reports `b` at `2026-01-05 00:00 .. 2026-01-06 00:00`. The project doesn't change: the next [`solve(None)`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#solve>) returns the cached result of the first solve.

When the user drops the task, send the same `SchedulingChanges` to `apply` and then `solve(None)`. See [Data Updates](<https://docs.dhtmlx.com/planning-engine/rust/live-projects/editing-a-project.md>).

[`PreviewOptions`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/preview-options.md>) takes the same diagnostic switches as [`SolveOptions`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/solve-options.md>):

- `subtree` previews only one branch.
- `reasons` adds a [`Reason`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/results/reason.md>) to each update, which tells you why a task would move.
- `analytics` adds the critical path and slack of the previewed state.

Previews stay sparse: there's no `include_unchanged` option. The full list is on the [`PreviewOptions` reference](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/preview-options.md>).

## Guarding with baseline tokens

A UI that previews in the background can run into a race. Another request applies an edit or reloads the data, and the preview now runs against a baseline the user never saw. Baseline tokens catch this.

Every `solve` and `level` result carries `baseline_id`, an opaque token for the accepted baseline. Pass it back in `PreviewOptions.baseline_id` (or `SchedulingChanges.baseline_id`). If the token no longer names the current baseline, `preview` fails with [`ProjectError::StaleBaseline`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/project-error.md>) instead of returning dates for the wrong state.

```rust
use plen_core::{
    DateTime, Error, Patch, PreviewOptions, Project, ProjectData, ProjectError, SchedulingChanges,
    SolveOptions, TaskData, TaskUpdate,
};

fn day(d: u32) -> Result<DateTime, Box<dyn std::error::Error>> {
    Ok(DateTime::from_ymd_hm(2026, 1, d, 0, 0).ok_or("invalid date")?)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut a = TaskData::new("a");
    a.start_date = Some(day(1)?);
    a.duration = Some(2.0);
    let mut data = ProjectData {
        tasks: vec![a],
        ..Default::default()
    };
    data.config.use_calendars = false;

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

    // The UI keeps the token of the dates it shows.
    let shown = loaded.baseline_id.clone();

    let mut resize = SchedulingChanges::default();
    let mut longer = TaskUpdate::new("a");
    longer.duration = Patch::Value(3.0);
    resize.tasks.update.push(longer);

    let guarded = PreviewOptions {
        baseline_id: shown.clone(),
        ..Default::default()
    };
    let preview = project.preview(&resize, &guarded)?;
    // A preview reports the baseline it read, so its token works for the next preview too.
    assert_eq!(preview.baseline_id, shown);

    // Meanwhile, another edit is committed.
    project.apply(&resize)?;

    // Until the next solve there is no baseline to preview against.
    assert!(matches!(
        project.preview(&resize, &PreviewOptions::default()),
        Err(Error::Project(ProjectError::NoBaseline))
    ));

    let latest = project.solve(None, &options)?;

    // The old token no longer names the baseline.
    match project.preview(&resize, &guarded) {
        Err(Error::Project(ProjectError::StaleBaseline)) => {
            // Refresh the view from `latest`, then preview with its token.
            let fresh = PreviewOptions {
                baseline_id: latest.baseline_id.clone(),
                ..Default::default()
            };
            project.preview(&resize, &fresh)?;
        }
        other => {
            other?;
        }
    }
    Ok(())
}
```

A few rules for tokens:

- Take the token from the latest [`solve`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#solve>) or [`level`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#level>) result. A preview result carries the same token it read, so a chain of previews during one drag can reuse it.
- The token changes whenever the data changes: new data, an [`apply`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#apply>), a solve that moves dates, leveling edits.
- Tokens are opaque. Don't parse them, store them across runs or compare them between processes. The same data can get a new token on a repeat run.
- [`preview`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#preview>) checks `PreviewOptions.baseline_id` and `SchedulingChanges.baseline_id` separately. Leave both as `None` to skip the check.

For how to handle `StaleBaseline` and the other project errors, see [Error handling](<https://docs.dhtmlx.com/planning-engine/rust/error-handling.md>).

## Preview with load

Set `PreviewOptions.profile` to a [`ProfileQuery`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/profile-query.md>) to see the resource load a change would cause. The result then has `profile`, the same per-bucket load and capacity that [`load_profile`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#load_profile>) returns, but computed on the previewed dates. Use it to show a resource histogram while the user drags a task.

```rust
use plen_core::{
    AssignmentData, DateTime, Patch, PreviewOptions, ProfileQuery, ProfileTasks, Project,
    ProjectData, ResourceData, SchedulingChanges, SolveOptions, TaskData, TaskUpdate,
};

fn day(d: u32) -> Result<DateTime, Box<dyn std::error::Error>> {
    Ok(DateTime::from_ymd_hm(2026, 1, d, 0, 0).ok_or("invalid date")?)
}

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

fn assign(task: &str) -> AssignmentData {
    AssignmentData {
        id: None,
        task: task.into(),
        resource: "dev".into(),
        value: 1.0,
        offset: None,
        duration: None,
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut data = ProjectData {
        tasks: vec![task("a", day(1)?, 2.0), task("b", day(3)?, 1.0)],
        resources: vec![ResourceData {
            id: "dev".into(),
            capacity: Some(1.0),
            calendar_id: None,
        }],
        assignments: vec![assign("a"), assign("b")],
        ..Default::default()
    };
    data.config.use_calendars = false;

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

    // Drag "b" one day earlier, onto the last day of "a".
    let mut drag = SchedulingChanges::default();
    let mut moved = TaskUpdate::new("b");
    moved.start_date = Patch::Value(day(2)?);
    drag.tasks.update.push(moved);

    let options = PreviewOptions {
        profile: Some(ProfileQuery {
            tasks: ProfileTasks::Over,
            ..ProfileQuery::new(day(1)?, day(5)?)
        }),
        ..Default::default()
    };
    let preview = project.preview(&drag, &options)?;

    if let Some(profile) = &preview.profile {
        for resource in &profile.resources {
            for bucket in resource.buckets.iter().filter(|b| b.load > b.capacity) {
                println!(
                    "{} overloaded at {}: {} of {} ({:?})",
                    resource.resource, bucket.start, bucket.load, bucket.capacity, bucket.tasks
                );
            }
        }
    }
    Ok(())
}
```

Both tasks need `dev` on January 2, so that bucket's load goes over its capacity of 1. With [`ProfileTasks::Over`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/profile-tasks.md>), each overloaded bucket also lists the tasks that share it.

The query covers buckets that start in `[from, to)`. By default it reports every resource, with the bucket size (`basis`) derived from `config.duration_unit`. Narrow it with `resources`, or switch `basis`. The query field has no `baseline_id` of its own: the preview checks `PreviewOptions.baseline_id`. Every preview also reports `overallocations` when the project has assignments, so you can skip the profile if you need only the overloaded windows. See [Resource load](<https://docs.dhtmlx.com/planning-engine/rust/resources-and-assignment/resource-load.md>) and the [`ProfileQuery` reference](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/profile-query.md>).

## Cloning a project

[`preview`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#preview>) takes one change set. To see the result of several edits in a row, each one solved before the next, clone the project and work on the copy.

```rust
use plen_core::{
    DateTime, LinkData, LinkType, Patch, Project, ProjectData, SchedulingChanges, SolveOptions,
    TaskData, TaskUpdate,
};

fn day(d: u32) -> Result<DateTime, Box<dyn std::error::Error>> {
    Ok(DateTime::from_ymd_hm(2026, 1, d, 0, 0).ok_or("invalid date")?)
}

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

fn update(id: &str, duration: f64) -> SchedulingChanges {
    let mut changes = SchedulingChanges::default();
    let mut update = TaskUpdate::new(id);
    update.duration = Patch::Value(duration);
    changes.tasks.update.push(update);
    changes
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut data = ProjectData {
        tasks: vec![task("a", day(1)?, 2.0), task("b", day(1)?, 1.0)],
        links: vec![LinkData {
            id: "ab".into(),
            source: "a".into(),
            target: "b".into(),
            link_type: LinkType::Fs,
            lag: 0.0,
            lag_unit: None,
        }],
        ..Default::default()
    };
    data.config.use_calendars = false;

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

    // A cheap copy: it shares state with `project` until it's edited.
    let mut draft = project.clone();
    draft.apply(&update("a", 4.0))?;
    draft.solve(None, &options)?;
    draft.apply(&update("b", 3.0))?;
    let outcome = draft.solve(None, &options)?;

    for update in &outcome.updates {
        if let (Some(start), Some(end)) = (update.start_date, update.end_date) {
            println!("draft task {}: {start} .. {end}", update.id);
        }
    }

    // The original is untouched and still previews against its own baseline.
    assert!(project
        .preview(&SchedulingChanges::default(), &Default::default())?
        .updates
        .is_empty());

    // The user accepts the draft: keep it as the project.
    project = draft;
    project.solve(None, &options)?;
    Ok(())
}
```

Edits on one copy never affect the other. The copy is a full [`Project`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md>) with its own baseline, cache and `baseline_id`, so you can preview, `level`, `evaluate` or keep editing it. To accept the draft, replace the original with it, or replay the same changes on the original with `apply`. To discard it, drop it.

Clone before [`level`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#level>) too. `level` takes `&mut self` and accepts the leveled dates as the new baseline, so running it on a copy is how you show the user a leveling proposal before committing it:

```rust
use plen_core::{LevelingOptions, Project};

fn leveling_proposal(project: &Project) -> Result<(), Box<dyn std::error::Error>> {
    let mut draft = project.clone();
    let leveled = draft.level(&LevelingOptions::default())?;
    for update in &leveled.schedule.updates {
        println!("task {} delay {:?}", update.id, update.leveling_delay);
    }
    Ok(())
}
```

The step-by-step version is in [Preview leveling](<https://docs.dhtmlx.com/planning-engine/rust/how-to/preview-leveling.md>). For comparing several alternative change sets with scores, use [`evaluate`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#evaluate>) instead, which works on disposable copies internally. See [Evaluating alternatives](<https://docs.dhtmlx.com/planning-engine/rust/resources-and-assignment/evaluating-alternatives.md>).

Cloning also helps with threads. To use one project from several threads, give each thread its own clone, or wrap the project in a `Mutex` or `RwLock`. `preview` takes `&self`, so with an `RwLock` several previews can run at once.
