Skip to content

Getting started ​

Install the package:

sh
pnpm add @springbird/effect-temporal   # or npm / yarn / bun

effect, @temporalio/client, and @temporalio/workflow are peer dependencies (modern package managers install them for you). You will also want @temporalio/worker to run a worker and @temporalio/testing for the test harness — both optional peers, used only where you use them.

Effect version

effect-temporal targets Effect v4 and pins its effect peer exactly (currently 4.0.0): the engine implements interfaces from effect/workflow (still @stability unstable upstream), whose API can move between releases. Match the pinned version; each release of this package states the one effect version it is built and tested against.

A Temporal deployment has three kinds of process, and this package has a module for each:

  • the workflow bundle — deterministic code Temporal replays; its entry file uses @springbird/effect-temporal/bundle, its handlers only @springbird/effect-temporal/definition
  • the worker — runs the bundle and your activities; registers via @springbird/effect-temporal/activities
  • clients — ordinary Node processes that start and observe workflows; use @springbird/effect-temporal/client

1. Define a workflow ​

A definition is a tag, a payload schema, an idempotency key, and success/error schemas. Both sides share this one module — keep it free of @temporalio/* imports.

ts
// definitions.ts — shared by the bundle and every client
import { Schema } from "effect";
import * as Workflow from "effect/workflow/Workflow";
import { defineActivity } from "@springbird/effect-temporal/definition";

export const Reserve = defineActivity("reserve", {
  payload: { sku: Schema.String, quantity: Schema.Finite },
  success: Schema.String,
  error: Schema.TaggedStruct("OutOfStock", { sku: Schema.String }),
  options: { startToCloseTimeout: "1 minute", retry: { maximumAttempts: 3 } },
});

export const OrderFlow = Workflow.make("orderFlow", {
  payload: { orderId: Schema.String, sku: Schema.String },
  idempotencyKey: ({ orderId }) => orderId,
  success: Schema.String,
  error: Reserve.errorSchema,
});

2. Author the body ​

The body is an Effect that runs inside the Temporal workflow sandbox. Workflows register themselves with Workflow.toLayer, and workflowBundle hosts every registration behind the bundle's default export — the same authoring that runs on Effect's cluster and in-memory engines.

ts
// workflows.ts — the workflow bundle (Temporal's workflowsPath points here)
import { Effect } from "effect";
import { workflowBundle } from "@springbird/effect-temporal/bundle";
import { sleep } from "@springbird/effect-temporal/definition";
import { OrderFlow, Reserve } from "./definitions.js";

const OrderFlowLive = OrderFlow.toLayer((payload) =>
  Effect.gen(function* () {
    // A declared activity, called directly: payload validated, result
    // decoded, typed failure lands in the error channel. Retries are
    // Temporal's, per the declaration's options.
    const reservation = yield* Reserve({ sku: payload.sku, quantity: 1 });
    yield* sleep({ name: "cooling-off", duration: "1 minute" });
    return `reserved:${reservation}`;
  }),
);

export default workflowBundle(OrderFlowLive); // Layer.mergeAll(...) for more

3. Implement the activities and run a worker ​

Activities are Effects too, bound to the same definitions. Every worker running these workflows also registers the attach bridge (makeEffectWorkflowActivities).

ts
// worker.ts
import { Effect } from "effect";
import { Client, Connection } from "@temporalio/client";
import { Worker } from "@temporalio/worker";
import {
  handle,
  implementActivities,
  makeEffectWorkflowActivities,
  type ActivityRunner,
} from "@springbird/effect-temporal/activities";
import { Reserve } from "./definitions.js";

// How your worker executes activity Effects — plug in your runtime,
// spans, and error reporting here.
const runner: ActivityRunner<never> = {
  run: (_name, _payload, effect) => Effect.runPromiseExit(effect),
};

const client = new Client({ connection: await Connection.connect() });

const worker = await Worker.create({
  taskQueue: "orders",
  workflowsPath: new URL("./workflows.js", import.meta.url).pathname,
  activities: {
    ...implementActivities(runner, [
      handle(Reserve, ({ sku, quantity }) => Effect.succeed(`${sku}x${quantity}`)),
    ]),
    ...makeEffectWorkflowActivities(client),
  },
});
await worker.run();

4. Start it from anywhere ​

WorkflowClient is the one client service: configure it once with a Temporal client and a default task queue.

ts
// api.ts — any ordinary Node process
import { Effect } from "effect";
import { Client, Connection } from "@temporalio/client";
import { layerWorkflowClient, WorkflowClient } from "@springbird/effect-temporal/client";
import { OrderFlow } from "./definitions.js";

const program = Effect.gen(function* () {
  const wf = yield* WorkflowClient;
  // Typed success/error; repeated calls with the same orderId attach to the
  // same execution and return the original result.
  return yield* wf.execute(OrderFlow, { orderId: "ord_123", sku: "sku-9" });
});

const client = new Client({ connection: await Connection.connect() });
await Effect.runPromise(
  program.pipe(Effect.provide(layerWorkflowClient({ client, taskQueue: "orders" }))),
);

That's the whole loop: definition → body → worker → client, with schemas holding every boundary.

Where to next ​

  • The runnable examples — each boots its own local Temporal dev server (pnpm run build, then pnpm --dir examples/<name> start): examples/order-saga is the one-shot saga (typed activities, compensation, approval, queryable state, idempotent attach, cancellation); examples/subscription is the long-lived entity (billing cycles, typed updates, mailbox cancellation, continue-as-new).
  • Defining workflows: idempotency, execution ids, start semantics.
  • Declaring capabilities: the definition module — one declaration per capability, one seam (WorkflowOps) engines implement.
  • Activities: declared activities, raw calls, failure and retry semantics.
  • Testing your app: the in-memory runtime, the typed fake client, and the live harness.
  • Lint rules: catch the authoring footguns mechanically.