[Canonical page](<https://docs.dhtmlx.com/planning-engine/rust/api/core/evaluate/task-finish/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# TaskFinish

The finish of one focus task in one evaluated stage. It's an entry of [`Finish::tasks`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/evaluate/finish.md>). It's returned by [`Project::evaluate`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#evaluate>) and isn't built by hand.

## Declaration

```rust
#[derive(Clone, Debug, PartialEq)]
pub struct TaskFinish {
    pub id: Id,
    pub finish: Option<Shift>,
}
```

With the `serde` feature (`cargo add plen-core --features serde`), `TaskFinish` also implements `Serialize` and `Deserialize`, with field names in camelCase.

## Fields

| Field | Type | Description |
|---|---|---|
| `id` | [`Id`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/data/id.md>) | The focus task's id, as given in [`EvaluateOptions::focus`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/evaluate/evaluate-options.md>). |
| `finish` | `Option<`[`Shift`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/evaluate/shift.md>)`>` | The task's finish date in this stage and its shift from the baseline finish. `None` when the task isn't in the stage, for example because the candidate removed it. |

## Example

Focus on task 2 and score two candidates: one makes task 2 longer, the other removes it.

```rust
use plen_core::{
    Candidate, DateTime, EvaluateOptions, 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 task = |id: i32| TaskData {
        start_date: Some(monday),
        duration: Some(2.0),
        ..TaskData::new(id)
    };
    let data = ProjectData {
        tasks: vec![task(1), task(2)],
        ..Default::default()
    };

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

    // Candidate "longer": task 2 takes 4 days instead of 2.
    let mut longer = SchedulingChanges::default();
    longer.tasks.update.push(TaskUpdate {
        duration: Patch::Value(4.0),
        ..TaskUpdate::new(2)
    });
    // Candidate "drop": task 2 is removed.
    let mut drop = SchedulingChanges::default();
    drop.tasks.remove.push(2.into());

    let candidates = [
        Candidate { id: "longer".into(), changes: longer },
        Candidate { id: "drop".into(), changes: drop },
    ];
    let options = EvaluateOptions {
        focus: vec![2.into()],
        ..Default::default()
    };

    let result = project.evaluate(&candidates, &options)?;
    for candidate in &result.candidates {
        let Some(scheduled) = &candidate.scheduled else { continue };
        for task in &scheduled.finish.tasks {
            match &task.finish {
                Some(shift) => println!(
                    "{}: task {} finishes {}, {:+} days",
                    candidate.id, task.id, shift.date, shift.delta
                ),
                None => println!("{}: task {} not in plan", candidate.id, task.id),
            }
        }
    }
    Ok(())
}
```

Output:

```
longer: task 2 finishes 2026-08-14 08:00, +2 days
drop: task 2 not in plan
```

## Related

- [`Finish`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/evaluate/finish.md>) - lists one entry per focus task
- [`EvaluateOptions`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/evaluate/evaluate-options.md>) - `focus` selects the tasks reported
- [`FocusOverload`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/evaluate/focus-overload.md>) - the overload around the same focus task
- [Evaluating Alternatives](<https://docs.dhtmlx.com/planning-engine/rust/resources-and-assignment/evaluating-alternatives.md#failed-and-cyclic-candidates>) - what a removed focus task looks like
