Skip to main content

Node

Trait Node 

Source
pub trait Node<TState, TStore = MemoryStore, TParams = DefaultParams>: Send + Sync
where TState: Clone + Debug + Send + Sync + 'static, TParams: Send + Sync + Clone, TStore: Send + Sync + 'static,
{ type PrepResult: Send + Sync; type ExecResult: Send + Sync; // Required methods fn prep<'life0, 'life1, 'async_trait>( &'life0 self, store: &'life1 TStore, ) -> Pin<Box<dyn Future<Output = Result<Self::PrepResult, CanoError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait; fn exec<'life0, 'async_trait>( &'life0 self, prep_res: Self::PrepResult, ) -> Pin<Box<dyn Future<Output = Self::ExecResult> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait; fn post<'life0, 'life1, 'async_trait>( &'life0 self, store: &'life1 TStore, exec_res: Self::ExecResult, ) -> Pin<Box<dyn Future<Output = Result<TState, CanoError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait; // Provided methods fn set_params(&mut self, _params: TParams) { ... } fn config(&self) -> TaskConfig { ... } fn run<'life0, 'life1, 'async_trait>( &'life0 self, store: &'life1 TStore, ) -> Pin<Box<dyn Future<Output = Result<TState, CanoError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait { ... } fn run_with_retries<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, store: &'life1 TStore, config: &'life2 TaskConfig, ) -> Pin<Box<dyn Future<Output = Result<TState, CanoError>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait { ... } }
Expand description

Node trait for workflow processing

This trait defines the core interface that all workflow nodes must implement. It provides type flexibility while maintaining performance and type safety.

§Generic Types

  • TState: The return type from the post method (typically an enum for workflow control)
  • TParams: The parameter type for this node (e.g., HashMap<String, String>)
  • TStore: The store backend type (e.g., MemoryStore)
  • PrepResult: The result type from the prep phase, passed to exec.
  • ExecResult: The result type from the exec phase, passed to post.

§Node Lifecycle

Each node follows a three-phase execution lifecycle:

  1. prep: Preparation phase - setup and data loading
  2. exec: Execution phase - main processing logic
  3. post: Post-processing phase - cleanup and result handling

The run method orchestrates these phases automatically.

§Benefits over String-based Approaches

  • Type Safety: Return enum values instead of strings
  • Performance: No string conversion overhead
  • IDE Support: Autocomplete for enum variants
  • Compile-Time Safety: Impossible to have invalid state transitions

§Example

use cano::prelude::*;

struct MyNode;

#[async_trait]
impl Node<String> for MyNode {
    type PrepResult = String;
    type ExecResult = bool;

    fn config(&self) -> TaskConfig {
        TaskConfig::minimal()  // Use minimal retries for fast execution
    }

    async fn prep(&self, _store: &MemoryStore) -> Result<Self::PrepResult, CanoError> {
        Ok("prepared_data".to_string())
    }

    async fn exec(&self, _prep_res: Self::PrepResult) -> Self::ExecResult {
        true // Success
    }

    async fn post(&self, _store: &MemoryStore, exec_res: Self::ExecResult)
        -> Result<String, CanoError> {
        if exec_res {
            Ok("next".to_string())
        } else {
            Ok("terminate".to_string())
        }
    }
}

Required Associated Types§

Source

type PrepResult: Send + Sync

Result type from the prep phase

Source

type ExecResult: Send + Sync

Result type from the exec phase

Required Methods§

Source

fn prep<'life0, 'life1, 'async_trait>( &'life0 self, store: &'life1 TStore, ) -> Pin<Box<dyn Future<Output = Result<Self::PrepResult, CanoError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Preparation phase - load data and setup resources

This is the first phase of node execution. Use it to:

  • Load data from store that was left by previous nodes
  • Validate inputs and parameters
  • Setup resources needed for execution
  • Prepare any data structures

The result of this phase is passed to the exec method.

Source

fn exec<'life0, 'async_trait>( &'life0 self, prep_res: Self::PrepResult, ) -> Pin<Box<dyn Future<Output = Self::ExecResult> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Execution phase - main processing logic

This is the core processing phase where the main business logic runs. This phase doesn’t have access to store - it only receives the result from the prep phase and produces a result for the post phase.

Benefits of this design:

  • Clear separation of concerns
  • Easier testing (pure function)
  • Better performance (no store access during processing)
§Retry Note

On any phase failure, the entire prepexecpost pipeline restarts. This method must be idempotent: if it has side effects (e.g. sending a network request or writing to an external system), those side effects will be repeated on every retry attempt.

Source

fn post<'life0, 'life1, 'async_trait>( &'life0 self, store: &'life1 TStore, exec_res: Self::ExecResult, ) -> Pin<Box<dyn Future<Output = Result<TState, CanoError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Post-processing phase - cleanup and result handling

This is the final phase of node execution. Use it to:

  • Store results for the next node to use
  • Clean up resources
  • Determine the next action/node to run
  • Handle errors from the exec phase

This method returns a typed value that determines what happens next in the workflow.

Provided Methods§

Source

fn set_params(&mut self, _params: TParams)

Set parameters for the node

Default implementation that does nothing. Override this method if your node needs to store or process parameters when they are set.

Source

fn config(&self) -> TaskConfig

Get the node configuration that controls execution behavior

Returns the TaskConfig that determines how this node should be executed. The default implementation returns TaskConfig::default() which configures the node with standard retry logic.

Override this method to customize execution behavior:

  • Use TaskConfig::minimal() for fast-failing nodes with minimal retries
  • Use TaskConfig::new().with_fixed_retry(n, duration) for custom retry behavior
  • Return a custom configuration with specific retry/parameter settings
Source

fn run<'life0, 'life1, 'async_trait>( &'life0 self, store: &'life1 TStore, ) -> Pin<Box<dyn Future<Output = Result<TState, CanoError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Run the complete node lifecycle with configuration-driven execution.

Orchestrates prepexecpost with the retry policy from Node::config. Only prep and post failures are retried; exec is infallible by design (returns Self::ExecResult directly). You can override this method for completely custom orchestration.

§Workflow integration

When a Node is registered with a crate::workflow::Workflow, the workflow engine uses the blanket crate::task::Task impl rather than calling this method directly. That blanket impl runs a single prepexecpost pass per attempt and delegates retries to the outer run_with_retries call in the workflow dispatcher.

If you call Node::run directly (outside a workflow), retries run here, which is correct for standalone use. Do not call Node::run inside a custom Task::run implementation — that would double-retry the node.

§Errors

Any error returned by prep or post is propagated after retries are exhausted.

Source

fn run_with_retries<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, store: &'life1 TStore, config: &'life2 TaskConfig, ) -> Pin<Box<dyn Future<Output = Result<TState, CanoError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Internal method to run the node lifecycle with retry logic

Executes the three phases (prepexecpost) in sequence, retrying the entire pipeline from prep whenever any phase returns an error.

§Full-Pipeline Retry Semantics

Unlike retry strategies that only re-run the failing step, this method restarts from the very beginning on each attempt:

  • If prep fails → the whole pipeline retries from prep.
  • If post fails → prep and exec both re-run before post is tried again.

This means all three phases must be idempotent when retries are enabled. Any side effects (network calls, writes to external systems, etc.) in prep or exec will be repeated on every retry attempt.

The number of attempts and delay between them are controlled by the TaskConfig returned from Node::config.

Implementors§