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.
- 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.
15-20 minutes
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.
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-pipeline
Your 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 = "2021"
[dependencies]
cloacina-workflow = { version = "0.10", features = ["packaged", "macros"] }
cloacina-workflow-plugin = "0.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
The compiler injects the shellNotice what is not here: no[lib] crate-type, no[features]section, nobuild.rs, no build-dependencies. Thecloacina-compilerservice injectscrate-type = ["cdylib", "rlib"]and thepackagedfeature at build time. Older documentation showed packages declaring these by hand — that model is retired. A package is just the two dependencies above plus your workflow source.
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"
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.
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-pipeline
cloacinactl package validate data-pipeline
cloacinactl package pack data-pipeline
# data-pipeline/data-pipeline.cloacina
validate checks the source tree against the canonical format without
uploading; pack produces the .cloacina source archive.
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 acme
The compiler claims the pending row, runs the cargo build with the injected shell, and writes the result back. Watch it:
cloacinactl compiler status
Once 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 ...
echo '{}' > /tmp/ctx.json
cloacinactl workflow run data_pipeline --tenant acme --context /tmp/ctx.json
# <execution-id>
cloacinactl execution status <execution-id> --tenant acme
# Status: Completed
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).
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
- Creating Your First Package — the task-focused how-to version of this flow
- Use cloacina-compiler Locally
- Explanation: Packaged Workflow Architecture
- CLI Reference