Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,31 @@ interfaces from `effect/unstable/*`, whose API can move between releases. Each r
of this package states the one `effect` version it is built and tested against, and
tracking a new `effect` release is a new release of this package.

## 0.2.0 (unreleased)
## 0.3.0 (unreleased)

- NEW: the `definition` module — declare each capability once and use it
directly inside handlers: `defineActivity` (callable: `yield* Charge({ orderId })`),
`defineDeferred` (`.await`), `defineMailbox` (`.take`/`.poll`),
`defineUpdate` (`.take`), `defineState` (`.set`), plus `version` (patch-marker
logic branches) and `evolved` (newest-first schema evolution with pure
migrations). Every primitive requires only the `WorkflowOps` service — the
one seam an engine implements — so handlers import nothing from
`engine-sandbox` and are engine-agnostic. Client-side driving uses the
declaration's underlying primitive (`U.update`, `M.mailbox`, `C.cell`,
`D.deferred`) with the existing `engine-client` ops.
- NEW: `makeTestWorkflowOps` in `testing` — an in-memory `WorkflowOps`
runtime (activities run their `handle` bindings with schema-validated
payloads; deferreds/mailboxes/updates/state driven via
`resolve`/`offer`/`request`/`stateOf`), so the same handler that runs on
Temporal runs in a plain unit test with no engine and no test server.
- `workflowBundle` provides the Temporal `WorkflowOps` runtime to hosted
layers automatically; bundle authoring is otherwise unchanged.
- The repository's fixtures, examples, and docs author with the `definition`
module throughout. The low-level per-primitive calls (`callActivity`,
`takeMailbox`, `pollMailbox`, `takeUpdate`, `setStateCell`) remain
exported from `engine-sandbox` as the machinery underneath.

## 0.2.0 (2026-08-27)

- BREAKING: workflow bundles are authored with `Workflow.toLayer`, hosted behind `workflowBundle(layer)` — one
dynamic default export per bundle, the same registration-driven authoring
Expand Down
10 changes: 5 additions & 5 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ workflow-semantics content).
| Sample | Status | With this package |
| --------------------------------------------------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [signals-queries](https://github.com/temporalio/samples-typescript/tree/main/signals-queries) | ✅ | `DurableDeferred.await` (signal) + `deferredState` (query) + `interrupt` (cancellation); custom state reads via `StateCell`. Tests: [primitives.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/primitives.test.ts), [mailbox.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/mailbox.test.ts). |
| [state](https://github.com/temporalio/samples-typescript/tree/main/state) | ✅ | Both halves: `DurableMailbox` for the repeated update signals, `StateCell` (`setStateCell` / `readStateCell`) for the query — read mid-flight and after completion. Test: [mailbox.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/mailbox.test.ts). |
| [mutex](https://github.com/temporalio/samples-typescript/tree/main/mutex) | ✅ | Lock workflow loops on `takeMailbox`, granting and collecting releases via workflow-to-workflow `offerMailbox`; the test asserts serialized critical sections across three contenders. Test: [mutex.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/mutex.test.ts). |
| [message-passing](https://github.com/temporalio/samples-typescript/tree/main/message-passing) | ✅ | `DurableUpdate` (`make` + `takeUpdate` + `executeUpdate`): request/response with typed success AND typed failure riding the update result. Test: [message-passing.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/message-passing.test.ts). |
| [state](https://github.com/temporalio/samples-typescript/tree/main/state) | ✅ | Both halves: `defineMailbox` for the repeated update signals, `defineState` (`.set` in the handler, `readStateCell` client-side) for the query — read mid-flight and after completion. Test: [mailbox.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/mailbox.test.ts). |
| [mutex](https://github.com/temporalio/samples-typescript/tree/main/mutex) | ✅ | Lock workflow loops on its mailbox's `.take`, granting and collecting releases via workflow-to-workflow `offerMailbox`; the test asserts serialized critical sections across three contenders. Test: [mutex.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/mutex.test.ts). |
| [message-passing](https://github.com/temporalio/samples-typescript/tree/main/message-passing) | ✅ | `defineUpdate` (`.take` in the handler + `executeUpdate` client-side): request/response with typed success AND typed failure riding the update result. Test: [message-passing.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/message-passing.test.ts). |
| [query-subscriptions](https://github.com/temporalio/samples-typescript/tree/main/query-subscriptions) | ✅ | `DurableMailbox` feeds the state, `StateCell` reads observe it evolving across repeated queries. Test: [mailbox.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/mailbox.test.ts). |
| [expense](https://github.com/temporalio/samples-typescript/tree/main/expense) (async activity completion) | 🟢 | Async completion is activity-side (`CompleteAsyncError` + client), untouched by the shim. |

Expand All @@ -37,7 +37,7 @@ workflow-semantics content).
| ------------------------------------------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [timer-progress](https://github.com/temporalio/samples-typescript/tree/main/timer-progress) | ✅ | `DurableClock.sleep`. Test: [primitives.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/primitives.test.ts) (delay skipped by the time-skipping server). |
| [sleep-for-days](https://github.com/temporalio/samples-typescript/tree/main/sleep-for-days) | ✅ | Same mechanism; a Temporal timer's duration is unbounded. Test: [primitives.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/primitives.test.ts) pins a 2-minute durable delay under time skipping — the same mechanism at any duration. |
| [timer-examples](https://github.com/temporalio/samples-typescript/tree/main/timer-examples) | ✅ | The order-timeout race is `Effect.raceFirst(activity, DurableClock.sleep)` — pinned by the `timeout-activity` fixture mode and the [primitives.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/primitives.test.ts) test "cancels the server-side activity when the calling FIBER is interrupted", which asserts the timed-out activity is cancelled server-side, not abandoned. The `UpdatableTimer` half races `takeMailbox` against the timer. Test: [mailbox.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/mailbox.test.ts). |
| [timer-examples](https://github.com/temporalio/samples-typescript/tree/main/timer-examples) | ✅ | The order-timeout race is `Effect.raceFirst(activity, DurableClock.sleep)` — pinned by the `timeout-activity` fixture mode and the [primitives.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/primitives.test.ts) test "cancels the server-side activity when the calling FIBER is interrupted", which asserts the timed-out activity is cancelled server-side, not abandoned. The `UpdatableTimer` half races the mailbox's `.take` against the timer. Test: [mailbox.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/mailbox.test.ts). |

## Workflow composition

Expand All @@ -46,7 +46,7 @@ workflow-semantics content).
| [child-workflows](https://github.com/temporalio/samples-typescript/tree/main/child-workflows) | ✅ | `MyChild.execute` in a workflow body; typed results/failures compose, cancellation cascades, discard = fire-and-forget, taken ids attach. Tests: [child.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/child.test.ts). |
| [saga](https://github.com/temporalio/samples-typescript/tree/main/saga) | ✅ | `Workflow.withCompensation`, firing on typed failure and on interrupt. Tests: [primitives.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/primitives.test.ts), [child.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/child.test.ts). |
| [continue-as-new](https://github.com/temporalio/samples-typescript/tree/main/continue-as-new) | ✅ | `continueAsNew(workflow, payload)` (engine-sandbox): ends the run and starts a fresh one with the same workflow id. Test: [continue-as-new.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/continue-as-new.test.ts) — asserts the fresh history, not just the looped result. |
| [batch-sliding-window](https://github.com/temporalio/samples-typescript/tree/main/batch-sliding-window) | ✅ | Sliding window of discarded children reporting completion via workflow-to-workflow mailbox; the orchestrator continues-as-new mid-batch, draining reports with `pollMailbox` into the carried in-flight set. Test: [batch.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/batch.test.ts). |
| [batch-sliding-window](https://github.com/temporalio/samples-typescript/tree/main/batch-sliding-window) | ✅ | Sliding window of discarded children reporting completion via workflow-to-workflow mailbox; the orchestrator continues-as-new mid-batch, draining reports with the mailbox's `.poll` into the carried in-flight set. Test: [batch.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/batch.test.ts). |
| [dsl-interpreter](https://github.com/temporalio/samples-typescript/tree/main/dsl-interpreter) | ✅ | The payload carries a declarative program (sequential steps of single or parallel activity calls) and Effect interprets it in-workflow. Test: [dsl.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/dsl.test.ts). |
| [early-return](https://github.com/temporalio/samples-typescript/tree/main/early-return) | ✅ | A forked fiber serves the confirmation `DurableUpdate` once authorization lands, while the main flow continues to the final result. Test: [early-return.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/early-return.test.ts). |
| [polling](https://github.com/temporalio/samples-typescript/tree/main/polling) | ✅ | The infrequent variant: the poll interval IS the activity retry policy, so the workflow is one `callRawActivity`. Test: [polling.test.ts](https://github.com/TeamSpringbird/effect-temporal/blob/main/src/__tests__/polling.test.ts). Frequent polling inside one activity is activity-side; unbounded polling adds `continueAsNew`. |
Expand Down
48 changes: 26 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,33 @@ The same pages live in [docs/](docs/) (`pnpm docs:dev` to browse locally).
import { Effect, Schema } from "effect";
import * as Workflow from "effect/unstable/workflow/Workflow";
import * as DurableClock from "effect/unstable/workflow/DurableClock";
import * as DurableDeferred from "effect/unstable/workflow/DurableDeferred";
import * as TypedActivity from "@springbird/effect-temporal/typed-activity";
import { callActivity, workflowBundle } from "@springbird/effect-temporal/engine-sandbox";
import { defineActivity, defineDeferred } from "@springbird/effect-temporal/definition";
import { workflowBundle } from "@springbird/effect-temporal/engine-sandbox";
import { WorkflowClient } from "@springbird/effect-temporal/client";

// Define once — shared by the workflow bundle, the worker, and every client.
// 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 = TypedActivity.make("charge", {
const Charge = defineActivity("charge", {
payload: { orderId: Schema.String },
success: Schema.String,
});
const ManagerApproval = DurableDeferred.make("manager-approval", {
const ManagerApproval = defineDeferred("manager-approval", {
success: Schema.String,
});

// The body is an Effect, running durably inside
// the Temporal sandbox — the same authoring Effect's other engines use:
// The body calls the declarations directly. Its only requirement is the
// WorkflowOps service — workflowBundle provides Temporal's; the testing
// module provides an in-memory one, so the same handler runs in a plain
// unit test with no engine.
const OrderFlowLive = OrderFlow.toLayer((payload) =>
Effect.gen(function* () {
const paid = yield* callActivity(Charge, { orderId: payload.orderId });
const paid = yield* Charge({ orderId: payload.orderId });
yield* DurableClock.sleep({ name: "cooling-off", duration: "3 days" });
const approver = yield* DurableDeferred.await(ManagerApproval);
const approver = yield* ManagerApproval.await;
return `${paid}:approved-by:${approver}`;
}),
);
Expand All @@ -65,20 +66,21 @@ pnpm add @springbird/effect-temporal # or npm / yarn / bun
contract over Temporal, for codebases that already run Temporal and do not
want a second durable-execution system (Effect's own `effect/unstable/cluster`
engine persists to its own SQL tables).
- **One package, tree-shakeable modules** — `@springbird/effect-temporal/engine-sandbox`
(workflow bundle), `/engine-client` + `/client` (ordinary Node),
`/typed-activity` + `/activities` (worker), `/testing`, `/nexus`, `/lint`.
Nexus, worker, and testing peers are optional.
- **One package, tree-shakeable modules** — `@springbird/effect-temporal/definition`
(declare capabilities once, engine-agnostic), `/engine-sandbox` (workflow
bundle), `/engine-client` + `/client` (ordinary Node), `/activities`
(worker), `/testing`, `/nexus`, `/lint`. Nexus, worker, and testing peers
are optional.
- **Typed end to end** — payloads, results, and *failures* are schemas at
every crossing: workflow results, activity calls, signals, queries, update
responses. Typed failures land in the Effect error channel on the reading
side; runs show red in the Temporal UI.
- **Entity workflows, complete** — `DurableMailbox` (repeated inbound
signals), `DurableUpdate` (request/response with typed success *and*
failure), `StateCell` (queryable published state, readable after the run
closes), `continueAsNew`, and patch-marker versioning
(`Versioning.match` chains) make the long-lived, observable, mutable
entity expressible end to end.
- **Entity workflows, complete** — `defineMailbox` (repeated inbound
signals), `defineUpdate` (request/response with typed success *and*
failure), `defineState` (queryable published state, readable after the run
closes), `continueAsNew`, patch-marker versioning (`version`), and schema
evolution (`evolved`) make the long-lived, observable, mutable entity
expressible end to end.
- **Cancellation that composes** — workflow cancel interrupts the handler
fiber (finalizers and `Workflow.withCompensation` run, their activity
calls still work), and workflow-internal interruption — `Effect.timeout`,
Expand All @@ -93,8 +95,10 @@ pnpm add @springbird/effect-temporal # or npm / yarn / bun
the workflow sandbox on a microtask scheduler; clocks, randomness, and
timers resolve to Temporal's replay-stable primitives. The mechanism is
documented in [How the engine works](https://www.effect-temporal.com/reference/how-it-works).
- **Testing story** — `makeFakeTemporalClient` (typed start/signal/
termination records, loud on everything unstubbed) for service tests, and
- **Testing story** — `makeTestWorkflowOps` (an in-memory `WorkflowOps`
runtime: the same handler that runs on Temporal runs in a plain unit test
with no engine), `makeFakeTemporalClient` (typed start/signal/termination
records, loud on everything unstubbed) for service tests, and
`startWorkflowTestHarness` over Temporal's time-skipping test server for
real workflow semantics.
- **Lint the footguns** — an oxlint/ESLint plugin ships in the package
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default defineConfig({
text: "Author",
items: [
{ text: "Defining workflows", link: "/guide/defining-workflows" },
{ text: "Declaring capabilities", link: "/guide/declaring-capabilities" },
{ text: "Activities", link: "/guide/activities" },
{ text: "Timers & approvals", link: "/guide/timers-and-approvals" },
{ text: "Child workflows", link: "/guide/child-workflows" },
Expand Down
Loading
Loading