RouterTask

Side-effect-free branching for your workflows.

A RouterTask is a processing model for pure routing: it reads Resources and returns the next TaskResult<TState> — and nothing else. No store mutations, no external I/O, no side effects. Because re-running a router is free, the workflow engine records no checkpoint row for it — see Crash Recovery for what that means. It is one of the Task family of processing models, alongside PollTask, BatchTask, and SteppedTask.

Key concept

A router's job is to decide, not to do. Keep route free of side effects and the engine treats branching as cost-free on resume — there is no recovery row to write, nothing to replay. If your branching logic also needs to write something, reach for a plain Task instead.


Quick Start with #[task::router]

The required method is async fn route(&self, res: &Resources) -> Result<TaskResult<TState>, CanoError>. Everything else has a default: fn config(&self) -> TaskConfig (defaults to TaskConfig::default()) and fn name(&self) -> Cow<'static, str> (defaults to the type name). The recommended form attaches #[task::router(state = MyState)] to an inherent impl block — the macro synthesises the impl RouterTask<MyState> for MyRouter header and emits a companion impl Task<MyState> for MyRouter so the same struct can also be passed to register if you ever want the checkpoint-recording behaviour back.

Inference form — #[task::router(state = ...)] on an inherent impl
use cano::prelude::*;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Step { Classify, FastPath, SlowPath, Done }

struct Config { use_fast_path: bool }
#[resource]
impl Resource for Config {}

struct Classifier;

#[task::router(state = Step)]
impl Classifier {
    async fn route(&self, res: &Resources) -> Result<TaskResult<Step>, CanoError> {
        let config = res.get::<Config, _>("config")?;
        if config.use_fast_path {
            Ok(TaskResult::Single(Step::FastPath))
        } else {
            Ok(TaskResult::Single(Step::SlowPath))
        }
    }
}
Tip

route may return TaskResult::Split as well as TaskResult::Single, so a router can fan a workflow out into parallel states. See Split & Join for what happens next.


Registering a Router

Register a router with Workflow::register_router(state, task)not register. The engine dispatches it exactly like an ordinary single-task state, but with one difference: it writes no CheckpointRow for the router state and consumes no checkpoint sequence number. A router has no side effects, so re-running it on resume costs nothing — there is nothing to recover, so there is nothing to record.

Wiring a router into a workflow
use cano::prelude::*;

let workflow = Workflow::new(resources)
    .register_router(Step::Classify, Classifier)   // router state — leaves no checkpoint row
    .register(Step::FastPath, FastProcessor)
    .register(Step::SlowPath, SlowProcessor)
    .add_exit_state(Step::Done);
Recovery interplay

On a checkpointed workflow, the recovery log skips router states entirely: a Start → Classify (router) → FastPath → Done run records rows for Start, FastPath, and Done — but not Classify. If a crash happens inside FastProcessor, resume_from re-enters at FastPath, having never needed to "remember" the routing decision — it just runs the router again on the way through if the resume point happens to land before it.


Explicit Trait-Impl Form

If you prefer to write the trait header yourself — e.g. for a generic impl, or a custom resource-key type — drop the state = ... argument and put a bare #[task::router] on a impl RouterTask<...> for ... block. Both forms emit the companion impl Task<...> for T.

Explicit form — #[task::router] on a trait impl
use cano::prelude::*;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Step { Classify, FastPath, SlowPath, Done }

struct Classifier;

#[task::router]
impl RouterTask<Step> for Classifier {
    fn name(&self) -> std::borrow::Cow<'static, str> {
        "classifier".into()
    }

    async fn route(&self, res: &Resources) -> Result<TaskResult<Step>, CanoError> {
        let config = res.get::<Config, _>("config")?;
        Ok(TaskResult::Single(if config.use_fast_path {
            Step::FastPath
        } else {
            Step::SlowPath
        }))
    }
}

Type-Erased Aliases

For dynamic dispatch — keeping a heterogeneous collection of routers, building one at runtime — Cano exports two aliases mirroring DynTask / TaskObject:

Alias Expands to
DynRouterTask<TState, TResourceKey> dyn RouterTask<TState, TResourceKey>
RouterTaskObject<TState, TResourceKey> Arc<dyn RouterTask<TState, TResourceKey>>

When to Use RouterTask

Reach for a RouterTask when:

If your branching logic also does work — writing to the store, calling an external system — use a plain Task and match-and-return the next state from run; that's the "Conditional Routing Task" pattern documented on the Tasks page, and those side effects do need a checkpoint, so a plain task is the right tool.

Runnable example

The crate ships a complete example — run it with cargo run --example router_task.