What's New in v0.5
New features
Section titled “New features”Cloudflare Workers runtime (@sayiir/cloudflare)
Section titled “Cloudflare Workers runtime (@sayiir/cloudflare)”Sayiir workflows now run natively inside Cloudflare Workers. The TypeScript surface (task, flow, Engine) mirrors sayiir-nodejs; execution is checkpoint-and-exit with signal/delay parking across requests and a cron-driven resumeAll sweep.
import { task, flow, Engine } from "@sayiir/cloudflare";
const onboarding = flow<number>("onboarding") .then(fetchUser) .then(sendEmail) .build();
export default { async fetch(request: Request, env: Env): Promise<Response> { const engine = await Engine.create(env.DB); const status = await engine.run(onboarding, "onboard-42", 42); return Response.json(status); }, async scheduled(event: ScheduledEvent, env: Env): Promise<void> { const engine = await Engine.create(env.DB); await engine.resumeAll(onboarding); },};Highlights:
- D1 persistence — Snapshots live in Cloudflare D1 (SQLite at the edge). No external database needed.
- Rust/WASM core — Orchestration, checkpointing, and execution run as compiled WASM. The same
sayiir-d1backend now drives both native SQLite (sqlx::SqlitePool) and D1 (sqlx_d1::D1Connection) over a genericSQLiteBackend<T>. resumeAllsweep — Picks up ready (expired delay/timeout), signalled (buffered event), and stale (no update withinstaleAfter) instances. Parked positions waiting on an external signal are excluded from the stale path so they aren’t re-resumed every cron tick.- Fork branches — Persist each branch’s final output under
branch_id. Parked branches collect intoExecutionPosition::AtFork { completed_branches, wake_at }; the join chain no longer double-executes, and a delayed branch wakes viadelay_wake_atinstead ofstaleAfterlater. - Durable RAG example —
examples/rag-agent-cfdemonstrates hybrid retrieval (Vectorize + D1 FTS5 + recency) via parallel fork, citation verification in a loop against R2-backed source text, and awaitForSignal5-minute human-clarify fallback with timeout. Built on Workers AI + Vectorize + R2 + D1.
Get started: Cloudflare Workers quick start.
conflictPolicy defaults to "fail" across all bindings
Section titled “conflictPolicy defaults to "fail" across all bindings”engine.run(workflow, instanceId, input) is not an idempotent alias for resume(). v0.5 makes this explicit: with the new default "fail" policy, calling run() against an instanceId that already has a snapshot returns an error instead of silently overwriting in-progress state. The correct action for a known instance is resume().
This applies to every binding — Rust runtime, sayiir-nodejs, sayiir-py, and @sayiir/cloudflare.
// default — rejects duplicate run() against an existing instanceconst engine = await Engine.create(env.DB);
// idempotent retries — return the existing instance's status without re-executingconst engine = await Engine.create(env.DB, { conflictPolicy: "use_existing" });
// force fresh start — delete the prior snapshot and any cancel/pause signalsconst engine = await Engine.create(env.DB, { conflictPolicy: "terminate_existing" });Rust callers pass the same options on the workflow client:
use sayiir_runtime::WorkflowClient;use sayiir_core::workflow::ConflictPolicy;
let client = WorkflowClient::new(backend) .with_conflict_policy(ConflictPolicy::UseExisting); // or ::TerminateExisting, ::Fail (default)A definition-hash mismatch against an existing snapshot is always a hard error regardless of policy — you can’t restart a different workflow under the same id.
Dependency injection with Deps (Rust)
Section titled “Dependency injection with Deps (Rust)”The Rust macros gained a first-class DI container. Tasks declared with #[inject] parameters can now be referenced directly in the workflow! macro — no more dropping down to WorkflowBuilder::then_task_with(Task::new(...)) to wire each service by hand.
use sayiir_runtime::prelude::*;use std::sync::Arc;
#[task(id = "charge")]async fn charge(order: Order, #[inject] stripe: Arc<Stripe>) -> Result<Receipt, BoxError> { stripe.charge(&order).await}
let deps = Deps::builder().insert(Arc::new(Stripe::new("sk_test"))).build();
let wf = workflow! { name: "checkout", deps: &deps, steps: [validate, charge, send_email]}.unwrap();The same Deps can be shared between a parent workflow and its flow-embedded children, so you construct each service exactly once. Missing dependencies surface as BuildError::MissingDep { task_id, type_name } at construction time — there is no runtime panic at first invocation.
Deps lives in sayiir_core::deps, re-exported via sayiir_runtime::prelude. See the Rust API reference and the composing workflows guide for details.
TaskRegistry::register_from_deps for task libraries and workers (Rust)
Section titled “TaskRegistry::register_from_deps for task libraries and workers (Rust)”The same DI plumbing is now available to code that builds a TaskRegistry by hand — typically task libraries and PooledWorker setup. TaskRegistry gained a generic register_from_deps::<T, _>(codec, &deps) method that delegates through a new DepsInjectable trait (auto-implemented by #[task]). It verifies dependencies first and returns Err(Vec<MissingDep>) if any are missing — the registry is never left partially populated:
pub fn billing_tasks( codec: Arc<JsonCodec>, deps: &Deps,) -> Result<TaskRegistry, Vec<MissingDep>> { let mut reg = TaskRegistry::new(); reg.register_from_deps::<ChargeTask, _>(codec.clone(), deps)?; reg.register_from_deps::<RefundTask, _>(codec, deps)?; Ok(reg)}See the reusable task libraries section for the full pattern.
Layering with Deps::merge
Section titled “Layering with Deps::merge”Deps::merge(other) and DepsBuilder::merge(other) let you combine containers — start from a base provided by a library, then layer in application-specific services before passing the result to workflow! { deps: … }. Entries in other win on type collision.
let mut deps = library::deps();deps.merge(application::deps());
// Or mid-build:let combined = Deps::builder() .merge(library::deps()) .insert(Arc::new(AppLogger::new())) .build();New exports
Section titled “New exports”Rust — new types in sayiir_runtime::prelude and sayiir_core::deps:
Deps, DepsBuilder, MissingDep, DepsInjectable (trait, auto-implemented by #[task]). New variants on sayiir_core::error::BuildError: MissingDep { task_id, type_name } and RegistryDepsConflict { task_id } (raised when workflow! { registry: …, deps: … } would re-register the same task via both sources). New method on TaskRegistry: register_from_deps::<T, _>(codec, &deps). New inherent methods on #[task]-generated structs: from_deps, verify_deps.