a3s-flow 0.4.0

Durable workflow engine and Rust SDK for A3S
Documentation
use a3s_flow::{
    FlowEngine, FlowRuntime, HookCallbackRoute, HookMetadata, HookStatus, RuntimeCommand,
    StepInvocation, WorkflowInvocation, WorkflowSpec,
};
use async_trait::async_trait;
use serde_json::json;
use std::sync::Arc;

struct ApprovalRuntime;

#[async_trait]
impl FlowRuntime for ApprovalRuntime {
    async fn run_workflow(
        &self,
        invocation: WorkflowInvocation,
    ) -> a3s_flow::Result<RuntimeCommand> {
        let ctx = invocation.context();
        if let Some(payload) = ctx.hook_payload("approval") {
            return Ok(ctx.complete(json!({
                "approved": payload["approved"],
                "reviewer": payload["reviewer"],
            })));
        }

        let invoice_id = ctx.input()["invoiceId"].as_str().unwrap_or("unknown");
        let metadata = HookMetadata::human_approval(format!("invoice:{invoice_id}"))
            .with_callback_route(HookCallbackRoute::post("/callbacks/flow/hooks/{token}"))
            .with_data("invoiceId", json!(invoice_id));

        Ok(ctx.create_hook_with_metadata(
            "approval",
            ctx.input()["approvalToken"]
                .as_str()
                .unwrap_or("approval-token"),
            metadata,
        )?)
    }

    async fn run_step(&self, _invocation: StepInvocation) -> a3s_flow::Result<serde_json::Value> {
        unreachable!("approval runtime does not schedule steps")
    }
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> a3s_flow::Result<()> {
    let engine = FlowEngine::in_memory(Arc::new(ApprovalRuntime));
    let spec = WorkflowSpec::rust_embedded("examples.approval", "0.1.0", "examples", "main");

    let run_id = engine
        .start_with_id(
            "approval-demo-0001",
            spec,
            json!({
                "invoiceId": "inv-0001",
                "approvalToken": "approval-token-0001",
            }),
        )
        .await?;
    let waiting = engine.snapshot(&run_id).await?;
    assert_eq!(waiting.hooks["approval"].status, HookStatus::Active);
    let approval_metadata = waiting
        .hook_metadata_as::<HookMetadata>("approval")?
        .expect("approval metadata");
    assert_eq!(
        approval_metadata.subject.as_deref(),
        Some("invoice:inv-0001")
    );
    println!("waiting_for_token={}", waiting.hooks["approval"].token);
    println!(
        "callback_route={}",
        approval_metadata
            .callback
            .as_ref()
            .map(|route| route.path.as_str())
            .unwrap_or("none")
    );

    let (resumed_run_id, hook_id) = engine
        .resume_hook_by_token(
            "approval-token-0001",
            json!({ "approved": true, "reviewer": "finance@example.com" }),
        )
        .await?;
    let completed = engine.snapshot(&resumed_run_id).await?;

    println!("resumed_hook={hook_id}");
    println!("status={:?}", completed.status);
    println!(
        "output={}",
        serde_json::to_string_pretty(&completed.output).unwrap()
    );
    Ok(())
}