09 — Working with the Workflow Registry
This is an optional advanced side-track, not a required step in the main
01–13 path. Everything before it runs workflows compiled into your own binary;
here you register a packaged workflow into Cloacina’s workflow registry and
execute it once from a runner that loads the package at runtime — no recompilation
of your application required. The registry stores .cloacina packages and lets a
DefaultRunner reconcile and run them dynamically. If runtime-loaded packages
aren’t something you need yet, skip ahead to
10 — Your First Computation Graph
and come back later. For the bigger picture of how versions are tracked, see
Workflow Versioning.
What you’ll learn
Section titled “What you’ll learn”- How to build a
.cloacinapackage from a workflow project in code - How to register a package through the DAL (
workflow_registry().register_workflow_package) - How to configure a
DefaultRunnerwith the registry reconciler enabled - How to execute a registry-loaded workflow once and inspect the result
Prerequisites
Section titled “Prerequisites”- The Cloacina repository checked out, with a Rust toolchain installed
- The ability to build a
.cloacinapackage — see Packaging Python Workflows
The complete example
Section titled “The complete example”The full source lives at examples/features/workflows/registry-execution.
To run it from the repository root:
cargo run --features sqlite -p registry-execution-demoThe rest of this tutorial walks through the path step by step.
Step 1: Build the .cloacina package
Section titled “Step 1: Build the .cloacina package”The registry stores packaged workflows, so the first step is to produce one. Here we
package the simple-packaged example project directly in code with
cloacina::packaging::package_workflow, returning the package bytes:
async fn build_package() -> Result<Vec<u8>, Box<dyn std::error::Error>> { let workspace_root = find_workspace_root()?; let project_path = workspace_root.join("examples/features/workflows/simple-packaged");
let temp_dir = TempDir::new()?; let output_path = temp_dir.path().join("package.cloacina");
cloacina::packaging::package_workflow(project_path, output_path.clone())?;
Ok(tokio::fs::read(output_path).await?)}Call it from main:
println!("📦 Building workflow package...");let package_data = build_package().await?;println!("✅ Package built: {} bytes", package_data.len());Result: the package bytes are in memory, ready to register. You’ll see the size printed, e.g. ✅ Package built: 1065567 bytes.
Step 2: Set up a database
Section titled “Step 2: Set up a database”The registry stores package metadata (and, with database storage, the binary too) in a Cloacina database. Use a fresh SQLite file so a stale run can’t dispatch an outdated package before the reconciler finishes loading:
let db_path = "/tmp/cloacina_demo.db";let _ = std::fs::remove_file(db_path);let db_url = format!("sqlite://{}", db_path);
let database = Database::new(&db_url, "", 5);database .run_migrations() .await .map_err(|e| format!("Migration error: {}", e))?;Result: a migrated SQLite database at /tmp/cloacina_demo.db that both the registry and the runner will share.
Step 3: Register the package through the DAL
Section titled “Step 3: Register the package through the DAL”Registration goes through the DAL. You create a DAL, choose a storage backend, obtain
a registry DAL, and call register_workflow_package with the package bytes:
// Create the DAL and choose a storage backend.let dal = cloacina::dal::DAL::new(database.clone());
// Database storage — stores binary data in the workflow_registry table.let storage = cloacina::dal::UnifiedRegistryStorage::new(database.clone());
// Filesystem storage is also available:// let storage = cloacina::dal::FilesystemRegistryStorage::new(storage_path);
let mut registry_dal = dal.workflow_registry(storage);
match registry_dal.register_workflow_package(package_data).await { Ok(package_id) => { println!("✅ Package registered with DAL ID: {}", package_id); } Err(RegistryError::PackageExists { package_name, version }) => { println!( "⚠️ Package already exists: {} v{} - continuing with existing package", package_name, version ); } Err(e) => return Err(e.into()),};Result: the package is stored in the registry. On the first run you’ll see ✅ Package registered with DAL ID: ...; on later runs the PackageExists arm reports that it already exists and continues.
You can confirm it landed by listing packages:
let packages = registry_dal.list_packages().await?;for package_info in &packages { println!( " - Package {} (v{}) - ID: {}", package_info.package_name, package_info.version, package_info.id );}Result: at least one package, e.g. - Package simple_demo (v1.0.0) - ID: ....
Step 4: Build a runner with the registry reconciler
Section titled “Step 4: Build a runner with the registry reconciler”DefaultRunnerConfig is built through its builder — its fields are private. Enable the
registry reconciler and point it at the same storage backend you registered with
(sqlite here, matching the database storage from Step 3):
let config = DefaultRunnerConfig::builder() .enable_registry_reconciler(true) .registry_storage_backend("sqlite") .build() .unwrap();
let runner = DefaultRunner::with_config(&db_url, config).await?;The reconciler runs in the background, scans the registry, loads workflow metadata, and registers the workflow’s tasks in the runtime. Give it a moment to finish startup reconciliation before executing:
let workflow_name = "data_processing"; // the workflow exported by simple-packagedtokio::time::sleep(tokio::time::Duration::from_secs(30)).await;Result: a running DefaultRunner that knows about the registered workflow.
Step 5: Execute the workflow once
Section titled “Step 5: Execute the workflow once”Build a context and execute the workflow by name. The runner resolves it from the registry-loaded tasks:
let mut context = Context::new();context.insert("demo", serde_json::json!("registry-execution"))?;
let result = runner.execute(workflow_name, context).await?;
println!("✅ Workflow executed successfully!");println!(" Execution ID: {}", result.execution_id);println!(" Status: {:?}", result.status);Result: the workflow runs end to end from the registry:
🚀 Executing workflow from registry...🔍 Collecting data...✅ Collected 1000 records⚙️ Processing data...✅ Processed 950 valid records📊 Generating report...✅ Report generated successfully
✅ Workflow executed successfully! Execution ID: 87654321-4321-8765-cba9-876543210987 Status: CompletedStep 6: Shut down
Section titled “Step 6: Shut down”Always shut the runner down cleanly so background tasks stop:
runner.shutdown().await?;Result: the runner stops, and the database at /tmp/cloacina_demo.db remains with the registered package for next time.
Where to go next
Section titled “Where to go next”You’ve registered a package and executed it once from the registry. The example source
also demonstrates cron scheduling of a registered workflow (via
runner.register_cron_workflow(...)) and gathering execution statistics — see the
full example
and Cron Scheduling. For how the
registry tracks workflow versions, see
Workflow Versioning.
Prev: 08 — Task Deferral · Next: 10 — Your First Computation Graph