Migrating from Library to Service Mode
This guide walks through converting an existing embedded Rust workflow into a packaged workflow for deployment. Library mode (embedded) means your application owns the Tokio runtime and calls Cloacina directly. Service mode (packaged) means the workflow is compiled as a shared library and loaded by the daemon or server.
- An existing workflow using the library/embedded tutorials (1-4)
- Familiarity with Packaged Workflows
| Aspect | Library Mode | Service Mode |
|---|---|---|
| Macro | #[workflow] |
#[workflow] (same — packaging is handled by build.rs and Cargo features) |
| Crate type | bin or lib |
cdylib (shared library) |
| Dependencies | cloacina (full crate) |
cloacina-workflow + cloacina-macros + cloacina-workflow-plugin |
| Registration | inventory::submit! entries seeded into Runtime at startup via seed_from_inventory() |
FFI vtable exports (9 methods, indices 0–8) loaded dynamically; the unified cloacina::package!() shell macro emits the entry points |
| Runtime | Your #[tokio::main] |
Daemon or server loads and runs it |
| Build | cargo build |
cloacina_build::configure() in build.rs |
Convert your binary crate to a library crate. Move your workflow module from main.rs to lib.rs:
Before (library mode):
my-workflow/
├── Cargo.toml
└── src/
└── main.rs # contains #[workflow] + #[tokio::main]
After (service mode):
my-workflow/
├── Cargo.toml
├── build.rs
└── src/
└── lib.rs # contains #[workflow] only
Change the crate type to cdylib and swap dependencies:
Before:
[package]
name = "my-workflow"
version = "0.1.0"
edition = "2021"
[dependencies]
cloacina = { version = "0.6.1", features = ["macros", "sqlite"] }
async-trait = "0.1"
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
After:
[package]
name = "my-workflow"
version = "0.1.0"
edition = "2021"
[features]
default = ["packaged"]
packaged = []
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
cloacina-macros = "0.6.1"
cloacina-workflow = { version = "0.6.1", features = ["packaged"] }
cloacina-workflow-plugin = "0.6.1"
async-trait = "0.1"
serde_json = "1.0"
[build-dependencies]
cloacina-build = "0.6.1"
# Optional: keep cloacina for local testing
[dev-dependencies]
cloacina = { version = "0.6.1", default-features = false, features = ["macros", "sqlite"] }
Key changes:
crate-type = ["cdylib", "rlib"]—cdylibproduces a shared library for dynamic loading;rliballowscargo testto workcloacina-workflowwith"packaged"feature — enables FFI export generationcloacina-build— generates the correct linker flags viabuild.rs- Removed
cloacinaandtokiofrom runtime dependencies (the host provides the runtime)
Create build.rs at the crate root:
fn main() {
cloacina_build::configure();
}
This sets the linker flags needed for the shared library to expose FFI entry points.
The workflow code itself barely changes. Remove the main() function and keep the #[workflow] module:
Before (main.rs):
use cloacina::*;
#[workflow(
name = "data_processing",
description = "Data processing pipeline"
)]
mod data_processing {
use super::*;
#[task(id = "extract", dependencies = [])]
async fn extract(context: &mut Context<serde_json::Value>) -> Result<(), TaskError> {
context.insert("data", serde_json::json!(42))?;
Ok(())
}
#[task(id = "transform", dependencies = ["extract"])]
async fn transform(context: &mut Context<serde_json::Value>) -> Result<(), TaskError> {
let data = context.get("data").unwrap().as_i64().unwrap();
context.insert("result", serde_json::json!(data * 2))?;
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let runner = DefaultRunner::new(":memory:").await?;
let result = runner.execute("data_processing", Context::new()).await?;
println!("Result: {:?}", result.status);
Ok(())
}
After (lib.rs):
use cloacina_workflow::{task, workflow, Context, TaskError};
#[workflow(
name = "data_processing",
description = "Data processing pipeline"
)]
pub mod data_processing {
use super::*;
#[task(id = "extract", dependencies = [])]
pub async fn extract(context: &mut Context<serde_json::Value>) -> Result<(), TaskError> {
context.insert("data", serde_json::json!(42))?;
Ok(())
}
#[task(id = "transform", dependencies = ["extract"])]
pub async fn transform(context: &mut Context<serde_json::Value>) -> Result<(), TaskError> {
let data = context.get("data").unwrap().as_i64().unwrap();
context.insert("result", serde_json::json!(data * 2))?;
Ok(())
}
}
Key differences:
- Import from
cloacina_workflowinstead ofcloacina - Module and functions are
pub(required for FFI visibility) - No
main()— the daemon/server provides the runtime - No
DefaultRunner— execution is managed by the host
Compile the shared library:
cargo build --release
This produces a shared library at target/release/libmy_workflow.so (Linux) or target/release/libmy_workflow.dylib (macOS).
To create a .cloacina package from the compiled library, use the packaging tools described in Packaged Workflows Tutorial.
Copy the .cloacina package to the daemon’s watch directory:
cp my-workflow.cloacina ~/.cloacina/packages/
Or upload to the server:
curl -X POST \
-H "Authorization: Bearer $API_KEY" \
-F "package=@my-workflow.cloacina" \
https://cloacina.example.com/v1/tenants/my_tenant/workflows
Add integration tests that use the full cloacina crate (via dev-dependencies):
#[cfg(test)]
mod tests {
use cloacina::DefaultRunner;
use cloacina_workflow::Context;
#[tokio::test]
async fn test_workflow_executes() {
let runner = DefaultRunner::new(":memory:").await.unwrap();
let result = runner
.execute("data_processing", Context::new())
.await
.unwrap();
assert_eq!(result.status.to_string(), "completed");
runner.shutdown().await;
}
}
- Crate type set to
["cdylib", "rlib"] -
build.rscallscloacina_build::configure() -
cloacina-workflowhas"packaged"feature enabled - Module and functions are
pub - No
main()inlib.rs -
cargo build --releaseproduces a shared library - Integration tests pass with
cargo test
- Packaged Workflows Tutorial — step-by-step packaging guide
- Workflow Registry Tutorial — managing packages in the registry
- FFI System — how dynamic loading works
- Packaged Workflow Architecture — design rationale