Activity
Named durable steps: all Activity.make options, return types, errors, retries and examples.
An activity records the outcome of one named step so completed work can be reused
when a workflow replays. Import Activity from @effect/workflow.
Activity.make(options)
| Property | Type | Required | Default / meaning |
|---|---|---|---|
name | string | Yes | Stable step identity within the workflow execution |
execute | Effect.Effect<A, E, R> | Yes | Work to perform; supply an Effect value, not an async callback |
success | Effect Schema for A | No | Schema.Void |
error | Effect Schema for E | No | Schema.Never |
interruptRetryPolicy | Schedule.Schedule<any, Cause.Cause<unknown>> | No | Upstream interruption retry policy, not a general business-error retry setting |
Returns an Activity that is itself yieldable as an Effect. yield* activity
obtains A or propagates E. The object also exposes name, successSchema,
errorSchema, exitSchema, execute and executeEncoded. Use the activity itself
to get the engine's durable boundary; invoking its raw execute bypasses that
recorded activity invocation.
The pinned default interruption policy combines exponential delays starting at 100 ms (factor 1.5) with a 10-second spaced schedule, limits recurrence to 10, and retries only interruption causes. Exhaustion becomes a defect. It does not automatically retry ordinary typed failures.
Example: fetch and validate a ticket
This exports a reusable step. Call fetchTicket("ticket-42", url) from a workflow.
The URL is an application-owned endpoint returning the shown JSON shape.
import { Activity } from "@effect/workflow";
import { Effect, Schema } from "effect";
const Ticket = Schema.Struct({ id: Schema.String, title: Schema.String });
export const fetchTicket = (key: string, url: string) => Activity.make({
name: key,
success: Ticket,
error: Schema.String,
execute: Effect.tryPromise({
try: async signal => {
const response = await fetch(url, { signal });
if (!response.ok) throw new Error(`Ticket endpoint returned ${response.status}`);
return Schema.decodeUnknownSync(Ticket)(await response.json());
},
catch: error => String(error),
}),
});Wrap a promise using Effect.tryPromise; map thrown values into your declared
failure schema. Effect.promise treats rejected promises as defects instead.
Use Schema.Void only for steps that intentionally return no value.
Retry and attempt identity
Activity.retry(activity, options) (also curried) accepts optional times: number
(maximum retries), while: (error) => boolean | Effect<boolean> (continue while
true), and until: (error) => boolean | Effect<boolean> (stop when true). It omits
the general Effect retry API's schedule property. Always bound retries explicitly
when repeating external work. Activity.CurrentAttempt
is an Effect context reference with initial value 1; it identifies the current
attempt. Decide whether the underlying action is safe to repeat before adding retries.
Activity.idempotencyKey(name, { includeAttempt?: boolean }) returns an Effect of
a deterministic key derived from the current execution and the supplied name.
With includeAttempt: true, it also incorporates the attempt number; the default
keeps the key stable across attempts. Use that distinction deliberately when
passing keys to external services.
Do not reuse an activity name for different loop iterations. Derive names such as
check-${index} from stable workflow input/control flow. Changing a function's
arguments while retaining a completed name does not invalidate its saved result.
What recording guarantees
An already recorded result can be reused. A process can still stop after an external service performed an action but before Laufwerk recorded the result. For external mutations, use the service's idempotency key or an independently verifiable receipt. Activity persistence alone cannot prove exactly-once delivery.
Use Effect.logInfo, Effect.annotateLogs and Effect.withSpan inside the body for
run-linked logs and traces. See Logging for the logging APIs and
Reliability for recovery behavior.