pub trait Node<TState, TStore = MemoryStore, TParams = DefaultParams>: Send + Syncwhere
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 theprepphase, passed toexec.ExecResult: The result type from theexecphase, passed topost.
§Node Lifecycle
Each node follows a three-phase execution lifecycle:
prep: Preparation phase - setup and data loadingexec: Execution phase - main processing logicpost: 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§
Sourcetype PrepResult: Send + Sync
type PrepResult: Send + Sync
Result type from the prep phase
Sourcetype ExecResult: Send + Sync
type ExecResult: Send + Sync
Result type from the exec phase
Required Methods§
Sourcefn 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 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.
Sourcefn 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 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 prep → exec → post 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.
Sourcefn 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,
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§
Sourcefn set_params(&mut self, _params: TParams)
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.
Sourcefn config(&self) -> TaskConfig
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
Sourcefn 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<'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 prep → exec → post 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 prep → exec → post 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
CanoError::Preparation—prepfailed on all retry attemptsCanoError::NodeExecution—postfailed on all retry attemptsCanoError::RetryExhausted— retry limit reached before a successful attempt
Any error returned by prep or post is propagated after retries are exhausted.
Sourcefn 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,
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 (prep → exec → post) 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
prepfails → the whole pipeline retries fromprep. - If
postfails →prepandexecboth re-run beforepostis 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.