# How tasks work

> A task is a typed graph of nodes and transitions that runs functions, prompts and agents, with fan-out, join and pause.

You will be able to read a task graph, enable the `swiftide-tasks` feature flag, and decide whether a job needs a task or a single agent. A task runs a fixed graph of steps, and each step can be a function, a prompt or an agent. [Your first task](/start/your-first-task/) builds a two-node task step by step if you want a smaller start.

## Showcase graph

The workspace-architect showcase drafts an `ARCHITECTURE.md` for a Cargo workspace. This function builds its whole task:

```rust title="examples/workspace-architect/src/graph.rs"
/// Builds the task: `WorkspaceRoot` in, the written ARCHITECTURE.md out.
pub fn architect_task<L>(
    llm: &L,
    settings: ArchitectSettings,
    progress: &Progress,
) -> Result<Task<WorkspaceRoot, WrittenDocument>, TaskError>
where
    L: ChatCompletion + SimplePrompt + Clone + 'static,
{
    let mut task = Task::new();

    let discover = task.register_node_fn({
        let progress = progress.clone();
        move |root: &WorkspaceRoot| {
            let members = workspace::discover(root, settings.selection)?;
            progress.step("discover", format_args!("{members} in {root}"));
            Ok::<_, workspace::DiscoveryError>(members)
        }
    });

    let analyse = task.register_node(CrateAnalyst::new(llm.clone(), progress.clone()));

    let join = task.register_node_fn({
        let progress = progress.clone();
        move |summaries: &JoinInput<CrateSummary>| {
            progress.joined(summaries.len());
            architecture_prompt(summaries)
        }
    });

    // `Arc<dyn SimplePrompt>` is a task node out of the box: `Prompt` in, `String` out.
    let render = task.register_node(Arc::new(llm.clone()) as Arc<dyn SimplePrompt>);

    let review = task.register_node(ApprovalGate::new(settings.pre_approved, progress.clone()));

    let write = task.register_node_async_fn({
        let output = settings.output;
        let progress = progress.clone();
        move |draft: &ArchitectureDraft| {
            let output = output.clone();
            let progress = progress.clone();
            Box::pin(async move {
                let written = draft.write_to(&output).await?;
                progress.step(
                    "write",
                    format_args!("{} ({} bytes)", written.path.display(), written.bytes),
                );
                Ok::<_, std::io::Error>(written)
            })
        }
    });

    task.starts_with(discover);

    // One branch per crate, all evaluating the same analyst node with a different input.
    task.register_transition(discover, move |members: WorkspaceMembers| {
        let mut crates = members.crates.into_iter();
        let Some(first) = crates.next() else {
            return Transition::error("the workspace has no crates to document");
        };

        crates
            .fold(Transition::fan_out(&analyse, first), |fan_out, member| {
                fan_out.and(&analyse, member)
            })
            .concurrency_model(ConcurrencyModel::Parallel)
            .join_with(join.join())
    })?;

    // Each branch hands its `CrateSummary` to the join; the join runs once all have arrived.
    task.register_transition(analyse, join.join())?;
    task.register_transition(join, {
        let progress = progress.clone();
        move |prompt| {
            progress.step("render", "drafting ARCHITECTURE.md");
            render.transitions_with(prompt)
        }
    })?;

    task.register_transition(render, move |markdown: String| {
        review.transitions_with(ArchitectureDraft::from_model_output(&markdown))
    })?;

    task.register_transition(review, move |decision| match decision {
        Review::Approved(draft) => write.transitions_with(draft).into(),
        Review::AwaitingApproval => Transition::pause(),
    })?;

    task.register_transition(write, task.transitions_to_finish())?;

    Ok(task)
}
```

Source: [`examples/workspace-architect/src/graph.rs#L35-L134`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/examples/workspace-architect/src/graph.rs#L35-L134)

The task has the type `Task<WorkspaceRoot, WrittenDocument>`. It takes a workspace root and returns the written document. Each `register_node*` call adds a node and returns its typed `NodeId`. Each `register_transition` call says where a node's output goes next.

- **discover** is a closure node. It runs `cargo metadata` and returns the member crates.
- **analyse** is a custom task node, `CrateAnalyst`. The fan-out sends every crate to it as its own branch. The fan-out sets `ConcurrencyModel::Parallel`, so the branches run at the same time. Each branch runs a fresh agent that reads one crate and returns a `CrateSummary`.
- **join** waits for every branch. It receives a `JoinInput<CrateSummary>` and builds the prompt for the writer.
- **render** is an `Arc<dyn SimplePrompt>` registered as a node. It sends the prompt to the model and returns Markdown.
- **review** is `ApprovalGate`. Until someone approves the draft, its transition returns `Transition::pause()` and the run stops.
- **write** is an async closure node. It saves the file, and its transition, `task.transitions_to_finish()`, ends the task.

In the captured run, ten analyst agents on `claude-sonnet-5-5` finished in the time of the slowest one. The join line from `OUTPUT.md`:

```text
[  40.5s] join     10 summaries; 230s of agent work in 41s wall-clock (5.7x)
```

Run it against the Swiftide workspace from the repository root:

```sh
export OPENAI_API_KEY=sk-...
cargo run -p workspace-architect
```

The command-line program starts the task with `task.run(WorkspaceRoot)`. That returns `TaskRunOutcome::Completed(WrittenDocument)` or `TaskRunOutcome::Paused`:

```rust title="examples/workspace-architect/src/main.rs"
let mut outcome = task.run(root).await?;
loop {
    match outcome {
        TaskRunOutcome::Completed(written) => return Ok(Some(written)),
        TaskRunOutcome::Paused => {
            if !approve_paused_draft(&task)? {
                return Ok(None);
            }
            outcome = task.resume().await?;
        }
    }
}
```

Source: [`examples/workspace-architect/src/main.rs#L125-L136`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/examples/workspace-architect/src/main.rs#L125-L136)

On a pause, the program asks you to approve the draft and then calls `resume()`.

## Task graph

![Task graph](/diagrams/diagram-task-graph.svg)

The diagram shows a smaller task with the same parts. A start node fans out to two prompt nodes that run in parallel. A join node collects both outputs, and the task finishes.

A task has these parts:

- A **task node** implements `TaskNode`. It declares an `Input`, an `Output` and an `Error` type, and its `evaluate` method turns a borrowed input into an output. [Nodes and transitions](/tasks/nodes-and-transitions/#task-nodes) covers the trait.
- A **transition** decides where a node's output goes next. It is a closure that takes the output by value, or a ready-made target such as `join.join()` or `task.transitions_to_finish()`. Each node has exactly one transition.
- The **start node** is set with `starts_with`. Its input type is the task's input type.
- A **fan-out** sends one input to each of several branches. Every fan-out names the join node that waits for all of them. By default the branches run one after another. Set `ConcurrencyModel::Parallel` on the fan-out or the task to run them at the same time.
- A **join** node receives the branch outputs as one `JoinInput<T>`. [Fan-out and join](/tasks/fan-out-and-join/) covers both.
- A **pause** stops the run, and `run` returns `TaskRunOutcome::Paused`. The paused branch keeps the node whose transition paused. `resume()` runs that node again with the same input. This is why the showcase pauses on the cheap review node and not on render. [Pause and resume](/tasks/pause-and-resume/) covers the details.

When a branch reaches the finishing transition, `run` or `resume()` returns `TaskRunOutcome::Completed` with the task's output. [Nodes and transitions](/tasks/nodes-and-transitions/#errors) covers the checks `run` makes before any node runs.

## Crate contents

The `swiftide-tasks` crate holds the graph and its runtime:

- `Task`, `TaskNode`, `NodeId` and `Transition`
- fan-out and join, with `JoinInput` and `AnyJoinInput`
- `ConcurrencyModel::Sequential` (the default) and `ConcurrencyModel::Parallel`
- pause, resume and reset, with `TaskRunOutcome`
- `TaskError` and `NodeError`
- closure nodes through `register_node_fn` and `register_node_async_fn`
- [built-in nodes](/tasks/built-in-nodes/) for `SimplePrompt`, `ChatCompletion` and `ToolExecutor` trait objects

The crate's own code spawns no tasks or threads. Parallel branches run concurrently inside the future that `run` returns. Swiftide itself still depends on tokio.

`TaskAgent`, which wraps an agent as a node, lives in swiftide-agents. Import it from `swiftide::agents::TaskAgent`.

## Feature flag

Turn on the `swiftide-tasks` feature flag:

```sh
cargo add swiftide@0.34 --features swiftide-tasks
```

The `swiftide-agents` flag turns it on as well. Import the graph types from `swiftide::tasks`. The old path `swiftide::agents::tasks` still works, but it is deprecated since 0.32.1. [Upgrading](/reference/upgrading/#tasks-crate) lists the 0.33 changes to tasks. A comment in the crate source says the tasks application programming interface (API) "is not stable yet" and "is subject to rapid change", so check Upgrading when you move to a new minor version.

## Tasks and agents

An [agent](/agents/overview/) runs a loop. After every model response it decides which tool to call next. It stops when a tool or the model asks it to, when it reaches its completion limit, or when it fails.

A task fixes the order of steps in code. Every node declares its input and output types. A transition to a node with the wrong input type does not compile, and neither does a fan-out without a join.

Control flow lives in Rust closures, not in model output. The model can still make decisions inside a node, but the graph decides what happens with the result. Each node does one job. You can test a closure node without a model, swap the model behind a prompt node, or give an agent node its own tools and limits.

Use a task when you know the steps in advance, when some of them need no model, or when you want to fan out work and collect it in one place. Put an agent inside a node for the open-ended part, such as reading one crate, and let the task handle the order, the parallel branches and the approval. [Agents as task nodes](/tasks/agents-as-nodes/) shows how. The [workspace-architect showcase](/showcases/workspace-architect/) walks through the whole program.

## Reference

- [`swiftide::tasks`](https://docs.rs/swiftide/0.34/swiftide/tasks/index.html)
- [`Task`](https://docs.rs/swiftide/0.34/swiftide/tasks/struct.Task.html)
- [`TaskNode`](https://docs.rs/swiftide/0.34/swiftide/tasks/trait.TaskNode.html)
- [`NodeId`](https://docs.rs/swiftide/0.34/swiftide/tasks/struct.NodeId.html)
- [`Transition`](https://docs.rs/swiftide/0.34/swiftide/tasks/struct.Transition.html)
- [`JoinInput`](https://docs.rs/swiftide/0.34/swiftide/tasks/struct.JoinInput.html)
- [`AnyJoinInput`](https://docs.rs/swiftide/0.34/swiftide/tasks/struct.AnyJoinInput.html)
- [`ConcurrencyModel`](https://docs.rs/swiftide/0.34/swiftide/tasks/enum.ConcurrencyModel.html)
- [`TaskRunOutcome`](https://docs.rs/swiftide/0.34/swiftide/tasks/enum.TaskRunOutcome.html)
- [`TaskError`](https://docs.rs/swiftide/0.34/swiftide/tasks/enum.TaskError.html)
- [`NodeError`](https://docs.rs/swiftide/0.34/swiftide/tasks/struct.NodeError.html)
- [`TaskAgent`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.TaskAgent.html)
- [`SimplePrompt`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.SimplePrompt.html)
- [`ChatCompletion`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.ChatCompletion.html)
- [`ToolExecutor`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.ToolExecutor.html)
- [Feature flags](/reference/feature-flags/)
- [Crates](/reference/crates/)

## Next

[Nodes and transitions](/tasks/nodes-and-transitions/)