Resources

Lifecycle-managed, typed dependency injection for Cano workflows.

Resources<TResourceKey> is the typed dictionary every workflow carries. Database pools, HTTP clients, config structs, MemoryStore, and per-run parameters are all resources — registered once, injected everywhere. Each lookup returns an Arc<R>, cheap to clone and safe to share across concurrent split tasks.

The engine calls setup() on each registered resource in insertion order before the FSM runs, and teardown() in reverse order after — even on failure. Tasks never receive resources directly; they retrieve typed handles from the injected &Resources<TResourceKey> via res.get::<R, _>(key)?.


Quick Start

Define a resource, register it, retrieve it by typed key in any task. The engine wires setup / teardown automatically.

End-to-end example
use cano::prelude::*;

#[derive(Debug, Hash, Eq, PartialEq)]
enum Key { Store, Config }

// Stateless resource — derive Resource for a no-op setup/teardown impl
#[derive(Resource)]
struct AppConfig { multiplier: u32 }

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

struct InitTask;

#[task(state = Step, key = Key)]
impl InitTask {
    async fn run(&self, res: &Resources<Key>) -> Result<TaskResult<Step>, CanoError> {
        let store  = res.get::<MemoryStore, _>(&Key::Store)?;
        let config = res.get::<AppConfig, _>(&Key::Config)?;

        store.put("value", 10u32 * config.multiplier)?;
        Ok(TaskResult::Single(Step::Done))
    }
}

#[tokio::main]
async fn main() -> Result<(), CanoError> {
    let resources = Resources::<Key>::new()
        .insert(Key::Store,  MemoryStore::new())
        .insert(Key::Config, AppConfig { multiplier: 3 });

    let workflow = Workflow::new(resources)
        .register(Step::Init, InitTask)
        .add_exit_state(Step::Done);

    workflow.orchestrate(Step::Init).await?;
    Ok(())
}

Runnable example: cargo run --example workflow_resources — a stateless config resource, a stateful counter resource (interior mutability), and a resource with real setup / teardown lifecycle work, all retrieved by type inside tasks.


Defining a Resource

The Resource trait gives every dependency three hooks — two lifecycle, one observability. All default to no-ops (and health() defaults to Healthy), so most resources need no manual impl. health() is opt-in observability — it is never called during normal workflow execution; see Observers & Health Probes.

Resource trait
pub trait Resource: Send + Sync + 'static {
    async fn setup(&self) -> Result<(), CanoError> { Ok(()) }
    async fn teardown(&self) -> Result<(), CanoError> { Ok(()) }
    async fn health(&self) -> HealthStatus { HealthStatus::Healthy }
}

Stateless — #[derive(Resource)]

Generates an empty impl Resource for T {}. Use it for config, parameter bags, read-only data — anything without lifecycle work.

Stateless resource via derive
#[derive(Resource)]
struct WorkflowParams {
    batch_size: usize,
    timeout_ms: u64,
}

Stateful — #[resource] on the impl block

Override setup / teardown for resources that open connections, allocate buffers, or flush state. Both hooks take &self: resources are shared via Arc, so mutation requires interior mutability — tokio::sync::Mutex, RwLock, atomics, etc.

Resource with custom setup/teardown
use cano::prelude::*;

use std::sync::{Arc, Mutex};

struct CounterResource {
    setup_count: Arc<Mutex<u32>>,
}

#[resource]
impl Resource for CounterResource {
    async fn setup(&self) -> Result<(), CanoError> {
        *self.setup_count.lock().unwrap() += 1;
        Ok(())
    }

    async fn teardown(&self) -> Result<(), CanoError> {
        // flush, close handles, etc.
        Ok(())
    }
}


Building a Resources Map

Resources is a typed builder. insert consumes self and panics on duplicate keys (programmer error). try_insert returns Result<Self, CanoError> so dynamic input can handle collisions explicitly. The two compose in a single chain.

insert + try_insert
// Static keys — duplicates indicate buggy wiring; let it panic
let resources = Resources::new()
    .insert("store",  MemoryStore::new())
    .insert("config", AppConfig::default())
    // Dynamic key — handle collision as data
    .try_insert(user_supplied_key, plugin)?;


Retrieving Resources in Tasks

get::<R, Q>(key) returns Result<Arc<R>, CanoError>. R names the resource type; Q is the key query type — use _ to let the compiler infer it from the key argument.

Recommended form — Q inferred
// String keys
let store  = res.get::<MemoryStore, _>("store")?;
let params = res.get::<WorkflowParams, _>("params")?;

// Enum keys
let store  = res.get::<MemoryStore, _>(&Key::Store)?;
let params = res.get::<WorkflowParams, _>(&Key::Params)?;

Error variantMeaning
ResourceNotFoundNo entry for the key. The resource was not inserted.
ResourceTypeMismatchKey present, but stored under a different type than R.
ResourceDuplicateKeyReturned by try_insert when the key is already present.

Declarative Loading with #[derive(FromResources)]

A struct of Arc dependencies can declare its lookups instead of calling res.get field-by-field. The derive generates fn from_resources(res: &Resources<K>) -> CanoResult<Self>.

Field-level #[res(...)] declares the lookup key — string literal for string-keyed maps, enum path for enum-keyed maps. Container-level #[from_resources(key = MyKey)] sets the key type when fields use enum keys.

Deps struct with enum keys
use cano::prelude::*;
use std::sync::Arc;

#[derive(Hash, Eq, PartialEq)]
enum Key { Store, Config }

#[derive(Resource)]
struct AppConfig { multiplier: u32 }

#[derive(FromResources)]
#[from_resources(key = Key)]
struct InitDeps {
    #[res(Key::Store)]
    store: Arc<MemoryStore>,
    #[res(Key::Config)]
    config: Arc<AppConfig>,
}

fn lookup(res: &Resources<Key>) -> Result<(), CanoError> {
    // In a task:
    let InitDeps { store, config } = InitDeps::from_resources(res)?;
    let _ = (store, config);
    Ok(())
}

Key Types: String vs Enum

TResourceKey defaults to Cow<'static, str>: &'static str literals stay borrowed (no allocation), owned Strings are accepted, lookups go through &str. Any type satisfying Hash + Eq + Send + Sync + 'static works — an enum is recommended for non-trivial workflows.

String keys (default)Enum keys
ConstructionResources::new()Resources::<Key>::new()
Typo detectionRuntime — ResourceNotFoundCompile time
Allocation on lookupNone for &str literalsNone
Best forPrototypes, small workflowsProduction, larger workflows

Runnable example: cargo run --example resources_advanced — enum-typed keys, try_insert returning CanoError::ResourceDuplicateKey on a repeated key, and the partial-LIFO rollback when a later resource's setup fails.


Lifecycle & Concurrency

When setup() and teardown() fire, how resources behave across concurrent splits, and standalone vs scheduler lifecycle are covered on a dedicated page:


Workflows Without Resources

When every task is self-contained, skip the resources map. Implement run_bare() instead of run(), and build the workflow with Workflow::bare() (equivalent to Workflow::new(Resources::empty())).

Resource-free workflow
use cano::prelude::*;

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

#[derive(Clone)]
struct PureTask;

#[task(state = Step)]
impl PureTask {
    async fn run_bare(&self) -> Result<TaskResult<Step>, CanoError> {
        let answer = 40 + 2;
        println!("Computed: {answer}");
        Ok(TaskResult::Single(Step::Done))
    }
}

fn build() -> Workflow<Step> {
    Workflow::bare()
        .register(Step::Compute, PureTask)
        .add_exit_state(Step::Done)
}

Runnable example: cargo run --example workflow_bare — a workflow with no resources at all.


API Reference

MethodSignatureNotes
new()Resources::new() -> SelfEmpty map.
empty()Resources::empty() -> SelfAlias for new().
with_capacity(n)Resources::with_capacity(n) -> SelfPre-allocates the underlying HashMap.
insertinsert<R: Resource>(self, key, resource) -> SelfBuilder; panics on duplicate.
try_inserttry_insert<R>(self, key, resource) -> Result<Self, CanoError>Returns ResourceDuplicateKey on duplicate.
getget<R, Q>(&self, key: &Q) -> Result<Arc<R>, CanoError>ResourceNotFound / ResourceTypeMismatch.
setup_all()async fn setup_all(&self) -> Result<(), CanoError>FIFO setup with partial LIFO rollback. Engine calls this.
teardown_all()async fn teardown_all(&self)LIFO teardown. Errors logged, never propagated.