<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nodes - Cano Documentation</title>
<meta name="description" content="Learn how to use Nodes in Cano - structured, resilient processing units with a three-phase lifecycle.">
<meta property="og:title" content="Nodes - Cano Documentation">
<meta property="og:description" content="Structured, resilient processing units with a three-phase lifecycle.">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@500;700&family=Fira+Code&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.6.1/mermaid.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-rust.min.js"></script>
<script src="script.js" defer></script>
<style>
.page-toc {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 0.75rem;
padding: 1.5rem 2rem;
margin-bottom: 3rem;
}
.page-toc-title {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--primary-color);
margin-bottom: 0.75rem;
}
.page-toc ol {
list-style: none;
counter-reset: toc-counter;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 0.25rem 2rem;
padding: 0;
margin: 0;
}
.page-toc li {
counter-increment: toc-counter;
}
.page-toc li a {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0;
color: #94a3b8;
font-size: 0.925rem;
font-weight: 500;
text-decoration: none;
transition: color 0.2s;
}
.page-toc li a::before {
content: counter(toc-counter, decimal-leading-zero);
font-size: 0.75rem;
font-weight: 700;
color: var(--border-color);
font-family: 'Fira Code', monospace;
transition: color 0.2s;
}
.page-toc li a:hover {
color: var(--primary-color);
}
.page-toc li a:hover::before {
color: var(--primary-color);
}
.section-divider {
border: none;
height: 1px;
background: linear-gradient(to right, var(--border-color), transparent);
margin: 3rem 0 0;
}
.callout {
padding: 1.25rem 1.5rem;
border-radius: 0.75rem;
margin: 1.5rem 0 2rem;
border-left: 4px solid;
font-size: 1rem;
}
.callout p {
margin: 0;
font-size: 1rem;
line-height: 1.6;
}
.callout-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 700;
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.callout-info {
background: rgba(56, 189, 248, 0.08);
border-color: var(--primary-color);
}
.callout-info .callout-label {
color: var(--primary-color);
}
.callout-tip {
background: rgba(52, 211, 153, 0.08);
border-color: #34d399;
}
.callout-tip .callout-label {
color: #34d399;
}
.callout-warning {
background: rgba(251, 191, 36, 0.08);
border-color: #fbbf24;
}
.callout-warning .callout-label {
color: #fbbf24;
}
.code-block {
position: relative;
margin: 1.5rem 0;
}
.code-block-label {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-bottom: 1px solid var(--code-bg);
border-radius: 0.5rem 0.5rem 0 0;
padding: 0.4rem 1rem;
font-size: 0.78rem;
font-weight: 600;
color: #94a3b8;
letter-spacing: 0.02em;
position: relative;
top: 1px;
z-index: 1;
}
.code-block-label .label-icon {
font-size: 0.85rem;
opacity: 0.7;
}
.code-block pre {
margin-top: 0 !important;
border-top-left-radius: 0 !important;
}
.lifecycle-flow {
display: grid;
grid-template-columns: 1fr auto 1fr auto 1fr;
gap: 0;
align-items: stretch;
margin: 2rem 0;
}
.lifecycle-phase {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 0.75rem;
padding: 1.5rem;
position: relative;
transition: border-color 0.2s, transform 0.2s;
}
.lifecycle-phase:hover {
border-color: var(--primary-color);
transform: translateY(-2px);
}
.phase-step {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.phase-number {
display: flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
border-radius: 50%;
font-weight: 700;
font-size: 0.8rem;
font-family: 'Fira Code', monospace;
flex-shrink: 0;
}
.phase-prep .phase-number {
background: rgba(56, 189, 248, 0.15);
color: var(--primary-color);
}
.phase-exec .phase-number {
background: rgba(129, 140, 248, 0.15);
color: var(--secondary-color);
}
.phase-post .phase-number {
background: rgba(52, 211, 153, 0.15);
color: #34d399;
}
.phase-step h3 {
margin: 0;
font-size: 1.15rem;
}
.phase-prep h3 { color: var(--primary-color); }
.phase-exec h3 { color: var(--secondary-color); }
.phase-post h3 { color: #34d399; }
.lifecycle-phase p {
font-size: 0.95rem;
margin: 0;
color: #94a3b8;
}
.phase-tag {
display: inline-block;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
padding: 0.15rem 0.5rem;
border-radius: 0.25rem;
margin-top: 0.75rem;
}
.phase-prep .phase-tag {
background: rgba(56, 189, 248, 0.1);
color: var(--primary-color);
}
.phase-exec .phase-tag {
background: rgba(129, 140, 248, 0.1);
color: var(--secondary-color);
}
.phase-post .phase-tag {
background: rgba(52, 211, 153, 0.1);
color: #34d399;
}
.lifecycle-arrow {
display: flex;
align-items: center;
justify-content: center;
padding: 0 0.5rem;
color: var(--border-color);
font-size: 1.25rem;
}
.lifecycle-arrow svg {
width: 24px;
height: 24px;
fill: none;
stroke: var(--border-color);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.styled-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: 2rem 0;
background: var(--card-bg);
border-radius: 0.75rem;
overflow: hidden;
border: 1px solid var(--border-color);
font-size: 0.95rem;
}
.styled-table thead tr {
background: rgba(255, 255, 255, 0.04);
}
.styled-table th {
padding: 1rem 1.25rem;
text-align: left;
font-weight: 600;
color: #e2e8f0;
font-size: 0.85rem;
letter-spacing: 0.03em;
text-transform: uppercase;
border-bottom: 1px solid var(--border-color);
}
.styled-table td {
padding: 0.875rem 1.25rem;
border-top: 1px solid rgba(255, 255, 255, 0.04);
color: #94a3b8;
}
.styled-table tbody tr:first-child td {
border-top: none;
}
.styled-table tbody tr:hover {
background: rgba(255, 255, 255, 0.02);
}
.pattern-section {
margin: 2rem 0;
}
.pattern-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.pattern-number {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 0.5rem;
background: rgba(129, 140, 248, 0.12);
color: var(--secondary-color);
font-weight: 700;
font-size: 0.85rem;
font-family: 'Fira Code', monospace;
flex-shrink: 0;
}
.pattern-header h3 {
margin: 0;
}
.config-cards .card {
border-left: 3px solid var(--secondary-color);
}
@media (max-width: 768px) {
.page-toc ol {
grid-template-columns: 1fr;
}
.page-toc {
padding: 1.25rem 1.25rem;
}
.lifecycle-flow {
grid-template-columns: 1fr;
gap: 0;
}
.lifecycle-phase {
padding: 1.25rem;
}
.lifecycle-arrow {
padding: 0.5rem 0;
transform: rotate(90deg);
}
.callout {
padding: 1rem 1.25rem;
}
.styled-table {
font-size: 0.85rem;
display: block;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.styled-table th,
.styled-table td {
padding: 0.625rem 0.75rem;
white-space: nowrap;
}
}
@media (max-width: 480px) {
.styled-table th,
.styled-table td {
padding: 0.5rem 0.625rem;
font-size: 0.8rem;
}
}
</style>
</head>
<body>
<button id="menu-toggle" class="menu-toggle" aria-label="Toggle navigation" aria-expanded="false">☰</button>
<div class="sidebar-overlay"></div>
<nav class="sidebar" role="navigation" aria-label="Main navigation">
<a href="index.html" class="logo">
<img src="logo.png" alt="" style="height: 24px; vertical-align: middle; margin-right: 8px;">
Cano
</a>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="task.html">Tasks</a></li>
<li><a href="nodes.html" class="active">Nodes</a></li>
<li><a href="workflows.html">Workflows</a></li>
<li><a href="store.html">Store</a></li>
<li><a href="scheduler.html">Scheduler</a></li>
<li><a href="tracing.html">Tracing</a></li>
</ul>
<div class="sidebar-footer">
<span class="version-badge">v0.8.0</span>
<div class="sidebar-links">
<a href="https://github.com/nassor/cano" title="GitHub Repository" aria-label="GitHub">GitHub</a>
<a href="https://crates.io/crates/cano" title="Crates.io" aria-label="Crates.io">Crates.io</a>
<a href="https://docs.rs/cano" title="API Documentation" aria-label="API Docs">Docs.rs</a>
</div>
</div>
</nav>
<main class="main-content">
<h1>Nodes</h1>
<p class="subtitle">Structured, resilient processing units with a three-phase lifecycle.</p>
<p>
A <code>Node</code> implements a structured three-phase lifecycle with built-in retry capabilities.
Nodes are ideal for complex operations where separating data loading, execution, and result handling improves clarity and maintainability.
</p>
<div class="callout callout-info">
<div class="callout-label">Key concept</div>
<p>
Nodes separate <em>what data to load</em> (prep), <em>how to process it</em> (exec), and <em>where to store results</em> (post).
On any phase failure the entire <code>prep</code> → <code>exec</code> → <code>post</code> pipeline is retried from scratch,
so all three phases must be idempotent.
</p>
</div>
<nav class="page-toc" aria-label="Table of contents">
<div class="page-toc-title">On this page</div>
<ol>
<li><a href="#three-phases">The Three Phases</a></li>
<li><a href="#implementing">Implementing a Node</a></li>
<li><a href="#nodes-vs-tasks">Nodes vs Tasks</a></li>
<li><a href="#patterns">Real-World Node Patterns</a></li>
<li><a href="#config">Configuration Best Practices</a></li>
</ol>
</nav>
<hr class="section-divider">
<h2 id="three-phases">The Three Phases</h2>
<div class="mermaid">
graph LR
A[Prep] -->|Load Data| B[Exec]
B -->|Process| C[Post]
C -->|Save Result| D[Next State]
</div>
<div class="lifecycle-flow">
<div class="lifecycle-phase phase-prep">
<div class="phase-step">
<span class="phase-number">1</span>
<h3>Prep</h3>
</div>
<p>Load data from the store, validate inputs, and setup resources. Returns <code>PrepResult</code>.</p>
<span class="phase-tag">Runs once</span>
</div>
<div class="lifecycle-arrow">
<svg viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</div>
<div class="lifecycle-phase phase-exec">
<div class="phase-step">
<span class="phase-number">2</span>
<h3>Exec</h3>
</div>
<p>Core processing logic. No store access — receives <code>PrepResult</code> and returns <code>ExecResult</code>. Must be idempotent.</p>
<span class="phase-tag">Must be idempotent</span>
</div>
<div class="lifecycle-arrow">
<svg viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</div>
<div class="lifecycle-phase phase-post">
<div class="phase-step">
<span class="phase-number">3</span>
<h3>Post</h3>
</div>
<p>Store results, cleanup resources, and determine the next workflow state based on execution outcome.</p>
<span class="phase-tag">Runs once</span>
</div>
</div>
<div class="callout callout-tip">
<div class="callout-label">Tip</div>
<p>
Keep IO operations (database reads, file access) in <code>prep</code> and <code>post</code>.
The <code>exec</code> phase has no store access by design — treat it as a pure computation.
Because the entire pipeline restarts on any failure, making all three phases idempotent
is the safest approach.
</p>
</div>
<hr class="section-divider">
<h2 id="implementing">Implementing a Node</h2>
<p>
Here is a complete example of a Node that generates random numbers and filters them.
This demonstrates the three-phase lifecycle: <code>prep</code> (generate), <code>exec</code> (filter), and <code>post</code> (store).
</p>
<div class="code-block">
<span class="code-block-label"><span class="label-icon">✎</span> Complete Node implementation</span>
<pre><code class="language-rust">use async_trait::async_trait;
use cano::prelude::*;
use rand::RngExt;
#[derive(Clone)]
struct GeneratorNode;
#[async_trait]
impl Node<WorkflowAction> for GeneratorNode {
// Define the types passed between phases
type PrepResult = Vec<u32>;
type ExecResult = Vec<u32>;
// Optional: Configure retry behavior
fn config(&self) -> TaskConfig {
TaskConfig::default().with_fixed_retry(3, Duration::from_secs(1))
}
// Phase 1: Preparation
// Load data, validate inputs, or generate initial state.
// This runs once and is not retried automatically.
async fn prep(&self, _store: &MemoryStore) -> Result<Self::PrepResult, CanoError> {
let mut rng = rand::rng();
let size = rng.random_range(25..=150);
let numbers: Vec<u32> = (0..size).map(|_| rng.random_range(1..=1000)).collect();
println!("Generated {} random numbers", numbers.len());
Ok(numbers)
}
// Phase 2: Execution
// Core logic. Infallible by design — returns ExecResult directly, not Result.
// If prep or post fail, the entire pipeline restarts; exec itself cannot fail.
async fn exec(&self, prep_res: Self::PrepResult) -> Self::ExecResult {
// Filter out odd numbers
let even_numbers: Vec<u32> = prep_res.into_iter().filter(|&n| n % 2 == 0).collect();
println!("Filtered to {} even numbers", even_numbers.len());
even_numbers
}
// Phase 3: Post-processing
// Store results, cleanup, and decide next state.
// It receives the result from exec().
async fn post(
&self,
store: &MemoryStore,
exec_res: Self::ExecResult,
) -> Result<WorkflowAction, CanoError> {
// Store the result in the shared memory store
store.put("filtered_numbers", exec_res)?;
println!("✓ Generator node completed");
Ok(WorkflowAction::Count)
}
}</code></pre>
</div>
<hr class="section-divider">
<h2 id="nodes-vs-tasks">Nodes vs Tasks</h2>
<p>
Every <code>Node</code> automatically implements <code>Task</code>, so you can use them interchangeably.
</p>
<table class="styled-table">
<thead>
<tr>
<th>Feature</th>
<th>Task</th>
<th>Node</th>
</tr>
</thead>
<tbody>
<tr>
<td>Structure</td>
<td>Single <code>run</code> method</td>
<td>3 phases: Prep, Exec, Post</td>
</tr>
<tr>
<td>Retry scope</td>
<td>Entire <code>run()</code> call</td>
<td>Entire <code>prep</code> → <code>exec</code> → <code>post</code> pipeline</td>
</tr>
<tr>
<td>Complexity</td>
<td>Low</td>
<td>Medium</td>
</tr>
<tr>
<td>Use Case</td>
<td>Simple logic, prototypes</td>
<td>Production logic, complex flows</td>
</tr>
</tbody>
</table>
<div class="callout callout-info">
<div class="callout-label">Blanket impl</div>
<p>
Because every <code>Node</code> automatically implements <code>Task</code>,
you can freely mix both types when calling <code>Workflow::register()</code>.
See the <a href="task.html">Tasks</a> page for the simpler interface.
</p>
</div>
<hr class="section-divider">
<h2 id="patterns">Real-World Node Patterns</h2>
<p>Nodes provide structure for complex workflows. Here are proven patterns from production systems.</p>
<section class="pattern-section">
<div class="pattern-header">
<span class="pattern-number">1</span>
<h3>ETL (Extract, Transform, Load) Pattern</h3>
</div>
<p>The three-phase lifecycle naturally maps to ETL operations.</p>
<div class="mermaid">
graph LR
A[Prep: Extract] -->|Load from source| B[Exec: Transform]
B -->|Process data| C[Post: Load]
C -->|Save to destination| D[Next State]
</div>
<div class="code-block">
<span class="code-block-label"><span class="label-icon">📦</span> ETL Node (illustrative — Record, ProcessedRecord, and database helpers are application-defined)</span>
<pre><code class="language-rust">use cano::prelude::*;
// Application-defined types and helpers (not part of Cano)
// struct Record { ... }
// struct ProcessedRecord { ... }
// async fn load_from_database(src: &str) -> Result<Vec<Record>, CanoError> { ... }
// async fn save_to_database(dst: &str, data: &[ProcessedRecord]) -> Result<(), CanoError> { ... }
// fn process_record(r: Record) -> ProcessedRecord { ... }
#[derive(Clone)]
struct ETLNode {
source: String,
destination: String,
}
#[async_trait]
impl Node<State> for ETLNode {
type PrepResult = Vec<Record>;
type ExecResult = Vec<ProcessedRecord>;
fn config(&self) -> TaskConfig {
TaskConfig::default()
.with_exponential_retry(3) // Retry failures
}
// Extract: Load data from source
async fn prep(&self, _store: &MemoryStore) -> Result<Self::PrepResult, CanoError> {
println!("📥 Extracting from: {}", self.source);
let records = load_from_database(&self.source).await?;
println!("Loaded {} records", records.len());
Ok(records)
}
// Transform: Process the data
async fn exec(&self, records: Self::PrepResult) -> Self::ExecResult {
println!("⚙️ Transforming {} records...", records.len());
records.into_iter()
.map(|r| process_record(r))
.collect()
}
// Load: Save to destination
async fn post(&self, store: &MemoryStore, processed: Self::ExecResult)
-> Result<State, CanoError> {
println!("📤 Loading to: {}", self.destination);
save_to_database(&self.destination, &processed).await?;
store.put("processed_count", processed.len())?;
Ok(State::Complete)
}
}</code></pre>
</div>
</section>
<section class="pattern-section">
<div class="pattern-header">
<span class="pattern-number">2</span>
<h3>Negotiation/Iterative Pattern</h3>
</div>
<p>Nodes can maintain state across iterations for negotiation workflows.</p>
<div class="mermaid">
sequenceDiagram
participant W as Workflow
participant S as SellerNode
participant B as BuyerNode
W->>S: Round 1
S-->>B: Offer $10,000
B-->>S: Counter: Too high
W->>S: Round 2
S-->>B: Offer $8,000
B-->>S: Accept ✓
</div>
<div class="code-block">
<span class="code-block-label"><span class="label-icon">💰</span> Negotiation Node</span>
<pre><code class="language-rust">#[derive(Clone)]
struct SellerNode;
#[async_trait]
impl Node<NegotiationState> for SellerNode {
type PrepResult = NegotiationState;
type ExecResult = NegotiationState;
async fn prep(&self, store: &MemoryStore) -> Result<Self::PrepResult, CanoError> {
// Load negotiation state or initialize
match store.get::<NegotiationState>("negotiation") {
Ok(state) => Ok(state),
Err(_) => Ok(NegotiationState::new(10000, 1000)), // initial price, budget
}
}
async fn exec(&self, mut state: Self::PrepResult) -> Self::ExecResult {
// Calculate new offer
if state.round > 1 {
let reduction = rand::random::<u32>() % 2000 + 500;
state.current_offer = state.current_offer.saturating_sub(reduction);
println!("Seller: New offer ${}", state.current_offer);
}
state
}
async fn post(&self, store: &MemoryStore, state: Self::ExecResult)
-> Result<NegotiationState, CanoError> {
store.put("negotiation", state.clone())?;
Ok(NegotiationState::BuyerEvaluate)
}
}</code></pre>
</div>
</section>
<section class="pattern-section">
<div class="pattern-header">
<span class="pattern-number">3</span>
<h3>Download & Analyze Pattern</h3>
</div>
<p>Perfect for workflows that download content and perform analysis.</p>
<div class="code-block">
<span class="code-block-label"><span class="label-icon">🔍</span> Download and analyze</span>
<pre><code class="language-rust">#[derive(Clone)]
struct BookAnalyzerNode;
#[async_trait]
impl Node<State> for BookAnalyzerNode {
type PrepResult = String; // Book content
type ExecResult = BookAnalysis;
fn config(&self) -> TaskConfig {
TaskConfig::default()
.with_fixed_retry(2, Duration::from_secs(1))
}
// Prep: Download book
async fn prep(&self, store: &MemoryStore) -> Result<Self::PrepResult, CanoError> {
let url: String = store.get("book_url")?;
println!("📥 Downloading book from: {}", url);
let client = reqwest::Client::new();
let content = client.get(&url)
.send().await?
.text().await?;
println!("Downloaded {} characters", content.len());
Ok(content)
}
// Exec: Analyze content (retried on failure)
async fn exec(&self, content: Self::PrepResult) -> Self::ExecResult {
println!("🔍 Analyzing content...");
let words: Vec<&str> = content.split_whitespace().collect();
let prepositions = count_prepositions(&words);
BookAnalysis {
word_count: words.len(),
preposition_count: prepositions,
density: (prepositions as f64 / words.len() as f64) * 100.0,
}
}
// Post: Store results
async fn post(&self, store: &MemoryStore, analysis: Self::ExecResult)
-> Result<State, CanoError> {
println!("📊 Analysis complete: {} words, {} prepositions",
analysis.word_count, analysis.preposition_count);
store.put("analysis", analysis)?;
Ok(State::Complete)
}
}</code></pre>
</div>
</section>
<section class="pattern-section">
<div class="pattern-header">
<span class="pattern-number">4</span>
<h3>Multi-Step Processing Pattern</h3>
</div>
<p>Chain multiple nodes together for complex data pipelines.</p>
<div class="code-block">
<span class="code-block-label"><span class="label-icon">⚙</span> Chained pipeline nodes</span>
<pre><code class="language-rust">// Node 1: Data Generator
#[derive(Clone)]
struct GeneratorNode;
#[async_trait]
impl Node<State> for GeneratorNode {
type PrepResult = ();
type ExecResult = Vec<u32>;
async fn prep(&self, _: &MemoryStore) -> Result<Self::PrepResult, CanoError> {
Ok(())
}
async fn exec(&self, _: Self::PrepResult) -> Self::ExecResult {
let mut rng = rand::rng();
(0..100).map(|_| rng.random_range(1..=1000)).collect()
}
async fn post(&self, store: &MemoryStore, data: Self::ExecResult)
-> Result<State, CanoError> {
store.put("generated_data", data)?;
Ok(State::Filter)
}
}
// Node 2: Data Filter
#[derive(Clone)]
struct FilterNode;
#[async_trait]
impl Node<State> for FilterNode {
type PrepResult = Vec<u32>;
type ExecResult = Vec<u32>;
async fn prep(&self, store: &MemoryStore) -> Result<Self::PrepResult, CanoError> {
store.get("generated_data")
}
async fn exec(&self, data: Self::PrepResult) -> Self::ExecResult {
data.into_iter().filter(|&x| x % 2 == 0).collect()
}
async fn post(&self, store: &MemoryStore, filtered: Self::ExecResult)
-> Result<State, CanoError> {
store.put("filtered_data", filtered)?;
Ok(State::Aggregate)
}
}
// Node 3: Aggregator
#[derive(Clone)]
struct AggregatorNode;
#[async_trait]
impl Node<State> for AggregatorNode {
type PrepResult = Vec<u32>;
type ExecResult = Stats;
async fn prep(&self, store: &MemoryStore) -> Result<Self::PrepResult, CanoError> {
store.get("filtered_data")
}
async fn exec(&self, data: Self::PrepResult) -> Self::ExecResult {
Stats {
count: data.len(),
sum: data.iter().sum(),
avg: data.iter().sum::<u32>() as f64 / data.len() as f64,
}
}
async fn post(&self, store: &MemoryStore, stats: Self::ExecResult)
-> Result<State, CanoError> {
store.put("final_stats", stats)?;
Ok(State::Complete)
}
}
// Combine in workflow
let workflow = Workflow::new(store.clone())
.register(State::Start, GeneratorNode)
.register(State::Filter, FilterNode)
.register(State::Aggregate, AggregatorNode)
.add_exit_state(State::Complete);</code></pre>
</div>
</section>
<hr class="section-divider">
<h2 id="config">Node Configuration Best Practices</h2>
<p>Choose the right configuration for your node's reliability requirements.</p>
<table class="styled-table">
<thead>
<tr>
<th>Config</th>
<th>Use Case</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>minimal()</code></td>
<td>Fast, reliable operations</td>
<td>Data transformations</td>
</tr>
<tr>
<td><code>default()</code></td>
<td>Standard operations</td>
<td>File I/O, Database queries</td>
</tr>
<tr>
<td><code>fixed_retry(n)</code></td>
<td>Transient failures</td>
<td>Network operations</td>
</tr>
<tr>
<td><code>exponential_retry(n)</code></td>
<td>Rate-limited APIs</td>
<td>External API calls</td>
</tr>
</tbody>
</table>
<div class="callout callout-warning">
<div class="callout-label">Important</div>
<p>
On any phase failure, the entire <code>prep</code> → <code>exec</code> → <code>post</code> pipeline
restarts from the beginning. All three phases must be idempotent — side effects in <code>prep</code> or
<code>exec</code> (e.g. writing to an external system) will be repeated on every retry attempt.
</p>
</div>
</main>
</body>
</html>