Effect-native
Workflows are schema-typed definitions, bodies are Effects, failures are typed channels. The whole Effect program runs deterministically inside the Temporal workflow sandbox.
Author workflows with Effect — schemas, typed errors, composition — and run them on Temporal's retries, timers, signals, and history.
import { Effect, Exit, Schema } from "effect";
import * as Workflow from "effect/workflow/Workflow";
import { defineActivity, defineDeferred, sleep } from "@springbird/effect-temporal/definition";
import { workflowBundle } from "@springbird/effect-temporal/bundle";
import { WorkflowClient } from "@springbird/effect-temporal/client";
// 1. Declare once: shared by the workflow bundle, the worker, and every client.
const OrderFlow = Workflow.make("orderFlow", {
payload: { orderId: Schema.String },
idempotencyKey: ({ orderId }) => orderId,
success: Schema.String,
});
const Charge = defineActivity("charge", {
payload: { orderId: Schema.String },
success: Schema.String,
});
const ManagerApproval = defineDeferred("manager-approval", {
success: Schema.String,
});
// 2. Author the body — call the declarations directly. The handler needs
// only WorkflowOps: workflowBundle provides Temporal's; the testing
// module provides an in-memory one for plain unit tests.
const OrderFlowLive = OrderFlow.toLayer((payload) =>
Effect.gen(function* () {
const paid = yield* Charge({ orderId: payload.orderId });
yield* sleep({ name: "cooling-off", duration: "3 days" });
const approver = yield* ManagerApproval.await;
return `${paid}:approved-by:${approver}`;
}),
);
export default workflowBundle(OrderFlowLive);
// 3. Drive it from ordinary Node — typed success, typed failure, idempotent;
// client-side ops take the declaration itself.
const wf = yield* WorkflowClient;
yield* wf.completeDeferred(ManagerApproval, workflowId, Exit.succeed("ben"));
const result = yield* wf.execute(OrderFlow, { orderId: "ord_123" });pnpm add @springbird/effect-temporal # or npm / yarn / bun