Workflow
A Workflow is a durable, database-backed DAG (directed acyclic graph) of Tasks with explicit dependencies. It is the thing you execute: its task states are persisted, claimed atomically, and recovered after a restart. The task is the unit of scheduling; the workflow is the unit you name, version, and run.
Mental model
Section titled “Mental model”- A workflow contains tasks; the dependencies between tasks form the DAG.
- A workflow has a name (how you execute it) and is content-versioned — its version derives from its tasks’ code and structure, so changes are explicit.
- A Runner executes a workflow against a database; a Context carries data between its tasks.
- Workflows are tenant-scoped (one tenant by default; many under the server).
Interfaces
Section titled “Interfaces”The same workflow, defined in each interface. Tasks are declared with the task decorator/macro and assembled into a named workflow.
In Rust, the #[workflow] module attribute names the workflow; the #[task]
functions inside the module are its tasks (registered in a global registry the
runner reads):
use cloacina::{task, workflow, Context, TaskError};
#[workflow(name = "greeting", description = "A one-task workflow")]pub mod greeting { use super::*;
#[task] pub async fn hello(context: &mut Context<serde_json::Value>) -> Result<(), TaskError> { context.insert("message", serde_json::json!("Hello World!"))?; Ok(()) }}In Python, the WorkflowBuilder context manager assembles tasks declared with
@cloaca.task inside its scope (auto-registered on exit):
import cloaca
with cloaca.WorkflowBuilder("greeting") as builder: builder.description("A one-task workflow")
@cloaca.task() def hello(context): context.set("message", "Hello World!") return contextDeclared inputs (params)
Section titled “Declared inputs (params)”A workflow can declare typed inputs so callers know what to pass and bad input
is caught before the run starts (rather than surfacing as a task failure mid-DAG).
Declared params derive a JSON Schema, surface on WorkflowDetail.declared_params,
and are validated at execute time — a missing required param or a top-level type
mismatch is rejected with 400 workflow_input_invalid. A workflow that declares
no params keeps free-form, unvalidated context.
A params(...) clause inside #[workflow]; each entry is name: Type (required)
or name: Type = default (optional). The type must be
serde::Serialize + schemars::JsonSchema (the schema is derived automatically):
#[workflow( name = "report", params( account_id: String, window_days: u32 = 30, ),)]pub mod report { /* … */ }The @cloaca.workflow_params(...) decorator, stacked above a @cloaca.task
function in the workflow’s packaged source; required is name=type, optional is
name=(type, default). It is parsed from source at build time (a runtime no-op),
so it must be present in the packaged source:
@cloaca.workflow_params(account_id=str, window_days=(int, 30))@cloaca.task(id="prepare")def prepare(context): ...See Declare Workflow Inputs for the full guide. (Validation is currently required-presence + top-level type; nested-schema validation is a planned follow-up.)
Key facts
Section titled “Key facts”- Naming: the name you register is the name you pass to
run/execute. - Versioning: content-derived; redefining a workflow’s tasks changes its version. See Workflow Versioning.
- Validation: the DAG is validated when built — missing tasks, cycles, and unresolvable dependencies are rejected.
- Execution semantics: at-least-once with recovery; tasks must be idempotent under redelivery.
Build one
Section titled “Build one”The concept lives here; learning to build and run a workflow lives in the doors:
- Embed it in your app → Embed · Tutorials
- Ship it to a server → Run the Service · Tutorials