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


# Quick start

By the end of this page, you'll have a Rust program that schedules two linked tasks, and a live project that reschedules them when one changes.

## Requirements

- A Rust toolchain with edition 2021. The crate doesn't declare a minimum Rust version.
- No async runtime. Every call is synchronous and CPU-only, with no I/O and no clock.

## Install

```bash
cargo add plen-core
```

The crate name is `plen-core`. In code you import it as `plen_core`.

## Schedule a snapshot

Start with the smallest useful plan: two tasks and a finish-to-start link. Both tasks start on Monday, 2026-08-10, at 08:00, so task 2 starts too early. It must wait for task 1 to finish.

```rust
use plen_core::{schedule, DateTime, LinkData, LinkType, ProjectData, SolveOptions, TaskData};

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

    let mut design = TaskData::new(1);
    design.start_date = Some(monday);
    design.duration = Some(2.0);

    let mut build = TaskData::new(2);
    build.start_date = Some(monday);
    build.duration = Some(1.0);

    let data = ProjectData {
        tasks: vec![design, build],
        links: vec![LinkData {
            id: 1.into(),
            source: 1.into(),
            target: 2.into(),
            link_type: LinkType::Fs,
            lag: 0.0,
            lag_unit: None,
        }],
        ..Default::default()
    };

    let result = schedule(data, &SolveOptions::default())?;
    for update in &result.updates {
        if let (Some(start), Some(end)) = (update.start_date, update.end_date) {
            println!("task {}: {} -> {}", update.id, start, end);
        }
    }
    Ok(())
}
```

Run it with `cargo run`:

```
task 2: 2026-08-12 08:00 -> 2026-08-13 08:00
```

[`schedule`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/schedule.md>) takes a [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/data/project-data.md>) snapshot and returns a [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/results/schedule-result.md>). Its `updates` list holds only the tasks that moved, as [`Mutation`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/results/mutation.md>) records. Task 1 didn't move, so it's not in the list.

Durations are working time in days. The default calendar works Monday to Friday, 08:00-12:00 and 13:00-17:00. That's why the two-day task 1 ends on Wednesday at 08:00, not on Tuesday evening.

Dates are [`DateTime`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/data/date-time.md>) values: local time, minute precision, no time zone. [`DateTime::from_ymd_hm`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/data/date-time.md#from_ymd_hm>) returns an `Option`, so the example turns `None` into an error instead of calling `unwrap()`. Ids are [`Id`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/data/id.md>) values, and `1.into()` and `"1".into()` name the same task.

### When the input is invalid

Every leaf task needs two of `start_date`, `end_date` and `duration`. If you leave out task 2's start date and keep only its duration, [`schedule`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/schedule.md>) rejects the snapshot before it schedules anything:

```
Error: Input(InputError { kind: Validation, message: "task needs two of start_date / end_date / duration", path: Some("tasks[1].start_date") })
```

The [`InputError`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/input-error.md>) has a `path` that points to the field that failed. Add the start date back and the program runs again.

Bad input is the only thing that fails this call. Dependency cycles, constraint conflicts and dropped links come back as data in [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/results/schedule-result.md>) (`cycles`, `conflicts`, `notes`), and the call still returns `Ok`.

## Keep a live project

[`schedule`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/schedule.md>) fits batch jobs and request handlers that see each snapshot once. An editor works differently. It loads the plan once, then sends small changes as the user works. That's what [`Project`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md>) does.

This program loads the same two tasks, previews a change, then commits it:

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

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

    let mut design = TaskData::new(1);
    design.start_date = Some(monday);
    design.duration = Some(2.0);

    let mut build = TaskData::new(2);
    build.start_date = Some(monday);
    build.duration = Some(1.0);

    let data = ProjectData {
        tasks: vec![design, build],
        links: vec![LinkData {
            id: 1.into(),
            source: 1.into(),
            target: 2.into(),
            link_type: LinkType::Fs,
            lag: 0.0,
            lag_unit: None,
        }],
        ..Default::default()
    };

    let mut project = Project::new(ProjectOptions::default());
    let first = project.solve(Some(data), &SolveOptions::default())?;
    println!("loaded, {} task(s) moved", first.updates.len());

    // The user stretches task 1 to three days.
    let mut stretch = TaskUpdate::new(1);
    stretch.duration = Patch::Value(3.0);
    let mut changes = SchedulingChanges::default();
    changes.tasks.update.push(stretch);

    let preview = project.preview(&changes, &Default::default())?;
    println!("preview: {} task(s) would move", preview.updates.len());

    let report = project.apply(&changes)?;
    if report.needs_resync {
        return Err("an edit was rejected; reload the project with solve(Some(data))".into());
    }

    let result = project.solve(None, &SolveOptions::default())?;
    for update in &result.updates {
        if let (Some(start), Some(end)) = (update.start_date, update.end_date) {
            println!("task {}: {} -> {}", update.id, start, end);
        }
    }
    Ok(())
}
```

Output:

```
loaded, 1 task(s) moved
preview: 2 task(s) would move
task 1: 2026-08-10 08:00 -> 2026-08-13 08:00
task 2: 2026-08-13 08:00 -> 2026-08-14 08:00
```

Each call does one job:

- [`Project::solve`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#solve>) with `Some(data)` loads and schedules the snapshot. Its dates become the project's baseline.
- [`Project::preview`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#preview>) runs the change on a copy of the baseline. The project doesn't change, so it's safe to call on every mouse move during a drag.
- [`Project::apply`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#apply>) commits the change and marks the project as unsolved.
- [`solve(None, ...)`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#solve>) reschedules the current state. Task 1 is reported because it was edited, and task 2 moves to follow it.

Edits are [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/changes/scheduling-changes.md>). A [`TaskUpdate`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/changes/task-update.md>) sets only the fields you give it. Each field is a [`Patch`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/changes/patch.md>): `Patch::Value` sets it, `Patch::Null` clears it, and `Patch::Missing` (the default) leaves it alone.

### When an edit is rejected

[`apply`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#apply>) accepts what it can and reports the rest in the returned [`ApplyReport`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/changes/apply-report.md>). It doesn't return an error for a bad operation. Remove a task that doesn't exist, and the report gets this entry in `rejected`:

```
tasks.remove[0] unknown task id
```

`needs_resync` is now `true`. Your copy of the plan and the project's copy no longer match, so the next [`solve(None, ...)`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#solve>) fails with [`Error::Project(ProjectError::NeedsResync)`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/error.md>) (displayed as `needs-resync`). To fix it, send the full data again with `solve(Some(data), ...)`. That's why the example checks `needs_resync` before it solves. [Recover from rejected changes](<https://docs.dhtmlx.com/planning-engine/rust/how-to/recover-from-rejected-changes.md>) covers this flow in detail.

## Where to go next

- [Project data](<https://docs.dhtmlx.com/planning-engine/rust/scheduling/project-data.md>): tasks, links, calendars, resources and assignments.
- [Scheduling a project](<https://docs.dhtmlx.com/planning-engine/rust/scheduling/scheduling-a-project.md>): one-shot and stateful scheduling, options and subtrees.
- [Reading results](<https://docs.dhtmlx.com/planning-engine/rust/scheduling/reading-results.md>): updates, conflicts, cycles and notes.
- [Working-time calendars](<https://docs.dhtmlx.com/planning-engine/rust/scheduling/working-time-calendars.md>): hours, weekdays and date exceptions.
- [Data Updates](<https://docs.dhtmlx.com/planning-engine/rust/live-projects/editing-a-project.md>): `apply`, baselines and resync.
- [Previewing changes](<https://docs.dhtmlx.com/planning-engine/rust/live-projects/previewing-changes.md>): what-if results without committing.
- [Leveling resources](<https://docs.dhtmlx.com/planning-engine/rust/resources-and-assignment/leveling-resources.md>): remove resource overallocation.
- [Error handling](<https://docs.dhtmlx.com/planning-engine/rust/error-handling.md>): [`InputError`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/input-error.md>), [`ProjectError`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/project-error.md>) and what's data instead of an error.
- [API reference](<https://docs.dhtmlx.com/planning-engine/rust/api/overview.md>): every public type and function.
