03 — Packaged Workflows
Welcome to the workflow packages tutorial! In this guide, you’ll author a Rust workflow package from the canonical scaffold, understand the compiler-injected shell model, and take the package all the way through pack → upload → server-side compile → execution.
Prerequisites
Section titled “Prerequisites”- Completion of 01 — Deploy a Server
— you’ll need the running server, the
acmetenant, and the profile from that tutorial. - Basic understanding of Rust and Cargo projects.
cloacinactl,cloacina-server, andcloacina-compilerbinaries on yourPATH.- A code editor of your choice.
Time Estimate
Section titled “Time Estimate”15-20 minutes
What Are Workflow Packages?
Section titled “What Are Workflow Packages?”A .cloacina package is a source archive (tar + bzip2). For Rust
packages, the shared library is compiled server-side by the
cloacina-compiler service after upload — you never build or ship a
cdylib yourself. See
Packaged Workflow Architecture
for the full rationale and trade-offs.
Scaffold the Package
Section titled “Scaffold the Package”cloacinactl package new emits the canonical source tree:
cloacinactl package new data-pipeline --lang rust# created rust workflow package at data-pipeline# next: cloacinactl package validate data-pipelineYour directory structure looks like this:
data-pipeline/├── Cargo.toml├── package.toml└── src/ └── lib.rs # Note: lib.rs, not main.rs!Look at the generated Cargo.toml — this is the whole build
configuration:
[package]name = "data-pipeline"version = "0.1.0"edition = "2024"
[dependencies]cloacina-workflow = { version = "0.12", features = ["packaged", "macros"] }cloacina-workflow-plugin = "0.12"serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"Alongside Cargo.toml sits package.toml, the package manifest the
server reads at upload:
[package]name = "data-pipeline"version = "0.1.0"
[metadata]workflow_name = "data_pipeline"description = "data-pipeline workflow"Understanding the Packaged Workflow
Section titled “Understanding the Packaged Workflow”Open src/lib.rs. The scaffold generates a two-task workflow:
use cloacina_workflow::{task, workflow};use cloacina_workflow::{Context, TaskError};
cloacina_workflow_plugin::package!();
#[workflow(name = "data_pipeline", description = "data-pipeline workflow")]pub mod data_pipeline_wf { use super::*;
#[task(id = "hello", dependencies = [])] pub async fn hello(context: &mut Context<serde_json::Value>) -> Result<(), TaskError> { context.insert("hello", serde_json::json!("world"))?; Ok(()) }
#[task(id = "goodbye", dependencies = ["hello"])] pub async fn goodbye(context: &mut Context<serde_json::Value>) -> Result<(), TaskError> { context.insert("done", serde_json::json!(true))?; Ok(()) }}Two things distinguish this from an embedded workflow:
cloacina_workflow_plugin::package!();at the crate root — this macro emits the FFI vtable a server uses to load the compiled library at runtime. Every Rust package has exactly this one line.- The workflow and task definitions themselves are identical to
the embedded form — same
#[workflow]module, same#[task]attributes, sameContextAPI.
Extend the Workflow
Section titled “Extend the Workflow”Add a third task so the package does something visible. Replace the
goodbye task’s dependency chain with a small pipeline — edit
src/lib.rs so the module body reads:
#[task(id = "collect_data", dependencies = [], retry_attempts = 2)] pub async fn collect_data(context: &mut Context<serde_json::Value>) -> Result<(), TaskError> { context.insert("records", serde_json::json!(1000))?; Ok(()) }
#[task(id = "process_data", dependencies = ["collect_data"], retry_attempts = 3)] pub async fn process_data(context: &mut Context<serde_json::Value>) -> Result<(), TaskError> { let records = context.get("records").cloned().unwrap_or_default(); context.insert("processed", records)?; Ok(()) }
#[task(id = "generate_report", dependencies = ["process_data"])] pub async fn generate_report(context: &mut Context<serde_json::Value>) -> Result<(), TaskError> { context.insert("report_done", serde_json::json!(true))?; Ok(()) }Check it compiles locally (optional — the server compiles it anyway, but a local build catches errors before upload):
cloacinactl package build data-pipelineValidate and Pack
Section titled “Validate and Pack”cloacinactl package validate data-pipelinecloacinactl package pack data-pipeline# data-pipeline/data-pipeline.cloacinavalidate checks the source tree against the canonical format without
uploading; pack produces the .cloacina source archive.
Start a Compiler and Upload
Section titled “Start a Compiler and Upload”Rust packages need a running cloacina-compiler service — without one,
an uploaded package sits at build_status = pending forever. Start one
against the same database as your server:
cloacinactl compiler start --database-url "$DATABASE_URL"Upload the package (uses the profile from tutorial 01):
cloacinactl package upload data-pipeline/data-pipeline.cloacina --tenant acmeThe compiler claims the pending row, runs the cargo build with the injected shell, and writes the result back. Watch it:
cloacinactl compiler statusOnce the build succeeds and the server’s reconciler runs (a few seconds), the workflow appears:
cloacinactl workflow list --tenant acme# data_pipeline v0.1.0 ...Run It
Section titled “Run It”echo '{}' > /tmp/ctx.jsoncloacinactl workflow run data_pipeline --tenant acme --context /tmp/ctx.json# <execution-id>
cloacinactl execution status <execution-id> --tenant acme# Status: CompletedVariations
Section titled “Variations”package publishcollapses build + pack + upload into one command:cloacinactl package publish data-pipeline.- Validate an archive (not just a source dir):
cloacinactl package validate data-pipeline/data-pipeline.cloacina. - Python packages skip the compiler entirely — the server loads
them directly. Scaffold one with
cloacinactl package new <name>(Python is the default--lang).
Next Steps
Section titled “Next Steps”You’ve authored a Rust workflow package from the canonical scaffold and run it through the full upload → compile → reconcile → execute pipeline.
Next: 04 — Packaging a Computation Graph
Related Resources
Section titled “Related Resources”- Creating Your First Package — the task-focused how-to version of this flow
- Use cloacina-compiler Locally
- Explanation: Packaged Workflow Architecture
- CLI Reference