Skip to content

01 — Your First Workflow

Build and run a single-task workflow embedded in your own process. The engine is identical in both languages; pick your tab.

A workflow named greeting with one task that writes a message into the Context, executed by a Runner against SQLite.

[dependencies]
cloacina = "0.12"
tokio = { version = "1", features = ["full"] }
serde_json = "1"

The backend is chosen at runtime from the database URL — no feature flags.

A Task is an async function with an id; a Workflow names a set of tasks.

The #[workflow] module attribute names the workflow; #[task] functions inside it are its tasks:

use cloacina::{task, workflow, Context, TaskError};
#[workflow(name = "greeting", description = "Say hello")]
pub mod greeting {
use super::*;
#[task]
pub async fn hello(ctx: &mut Context<serde_json::Value>) -> Result<(), TaskError> {
ctx.insert("message", serde_json::json!("Hello World!"))?;
Ok(())
}
}

Create a Runner against SQLite and execute the workflow by name.

use cloacina::executor::WorkflowExecutor; // brings `execute` into scope
use cloacina::runner::{DefaultRunner, DefaultRunnerConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let runner = DefaultRunner::with_config(
"sqlite://app.db",
DefaultRunnerConfig::default(),
).await?;
let result = runner.execute("greeting", Context::new()).await?;
println!("status: {:?}", result.status);
println!("context: {:?}", result.final_context);
runner.shutdown().await?;
Ok(())
}

You’ll see a completed status and the message value in the final context.

  • A task is an async function with an id; a workflow names a set of tasks.
  • A runner executes a workflow against a database (SQLite here) and persists state — so execution survives restarts.