Skip to main content
Task.define() creates a TaskDefinition — the core building block of @durable-effect/task. You provide Effect Schemas for state and events, then write handlers that run inside a TaskContext.

Signature

Task.define<S, E, EErr, AErr, R, OErr, GErr>(
  config: TaskDefineConfig<S, E, EErr, AErr, R, OErr, GErr>
): TaskDefinition<S, E, EErr, AErr, R, OErr, GErr>
The type parameters are inferred from your config — you rarely need to supply them explicitly.
ParameterDescription
SState type
EEvent type
EErrError type from onEvent
AErrError type from onAlarm
REffect service requirements shared across handlers
OErrError type from onError (default never)
GErrError type from onClientGetState (default never)

Config fields

state
PureSchema<S>
required
Effect Schema for the task’s persisted state. Must be a pure schema — no service dependencies for encoding or decoding.
event
PureSchema<E>
required
Effect Schema for incoming events. Same purity requirement as state.
onEvent
(ctx: TaskContext<S>, event: E) => Effect<void, EErr, R>
required
Called when an event is sent to this task via send(). Use ctx to read and write state, schedule alarms, or terminate the task. The handler runs inside the Effect runtime backed by Durable Object storage.
onAlarm
(ctx: TaskContext<S>) => Effect<void, AErr, R>
required
Called when a previously scheduled alarm fires. Cloudflare invokes this automatically when the alarm time arrives. Use ctx.scheduleIn() or ctx.scheduleAt() inside this handler to re-schedule.
onError
(ctx: TaskContext<S>, error: EErr | AErr) => Effect<void, OErr, R>
Optional. Called when onEvent or onAlarm fails. Receives the full TaskContext<S> and the raw error value. Use it to save recovery state, log failures, or perform cleanup.If omitted, unhandled errors propagate as TaskExecutionError.
onClientGetState
(ctx: TaskContext<S>, state: S | null) => Effect<S | null, GErr, R>
Optional. Intercepts external getState() calls before the state is returned to the client. The hook receives the full TaskContext<S> and the raw persisted state, and must return S | null.This hook does not affect ctx.recall() inside your handlers — it only runs when an external caller invokes getState().Common uses: redact sensitive fields, compute derived values, or enrich state from external services.

Return type

Task.define() returns a TaskDefinition<S, E, EErr, AErr, R, OErr, GErr>:
interface TaskDefinition<S, E, EErr, AErr, R, OErr = never, GErr = never> {
  readonly _tag: "TaskDefinition"
  readonly state: PureSchema<S>
  readonly event: PureSchema<E>
  readonly onEvent: (ctx: TaskContext<S>, event: E) => Effect<void, EErr, R>
  readonly onAlarm: (ctx: TaskContext<S>) => Effect<void, AErr, R>
  readonly onError?: (ctx: TaskContext<S>, error: EErr | AErr) => Effect<void, OErr, R>
  readonly onClientGetState?: (ctx: TaskContext<S>, state: S | null) => Effect<S | null, GErr, R>
}
Pass this value directly to createTasks(), or wrap it with withServices() first if your handlers depend on Effect services.

Purity constraint

State and event schemas must satisfy PureSchema<T>, which requires that encoding and decoding have no service dependencies (DecodingServices and EncodingServices are both never):
type PureSchema<T> = Schema.Top & {
  readonly "Type": T
  readonly "DecodingServices": never
  readonly "EncodingServices": never
}
This constraint exists so task data can be serialized and deserialized independently of the Effect runtime — for example, when storing state in Durable Object storage or decoding an incoming event payload over HTTP.
// Good — pure schemas work fine
const MyState = Schema.Struct({ count: Schema.Number })
const MyEvent = Schema.Struct({ _tag: Schema.Literal("Increment"), amount: Schema.Number })

// Bad — schemas with service requirements won't compile
Standard Schema.Struct, Schema.Number, Schema.String, Schema.Literal, and similar built-in schemas are always pure.

withServices(definition, layer)

If your handlers require Effect services (databases, HTTP clients, etc.), use withServices() to provide a Layer and eliminate the R type parameter before passing to createTasks():
import { withServices } from "@durable-effect/task"

const myTask = Task.define({
  state: MyState,
  event: MyEvent,
  onEvent: (ctx, event) =>
    Effect.gen(function* () {
      const db = yield* DatabaseService
      // ...
    }),
  onAlarm: () => Effect.void,
})

// Eliminate R — required before passing to createTasks()
const myTaskResolved = withServices(myTask, DatabaseServiceLive)

export const { TasksDO, tasks } = createTasks({
  myTask: myTaskResolved,
})
Signature:
function withServices<S, E, EErr, AErr, R, OErr, GErr>(
  definition: TaskDefinition<S, E, EErr, AErr, R, OErr, GErr>,
  layer: Layer<R>,
): TaskDefinition<S, E, EErr, AErr, never, OErr, GErr>
withServices() wraps each handler with Effect.provide(handler, layer), returning a new TaskDefinition with R set to never. The state and event schemas are unchanged.

Error behavior

Without onError, any exception thrown (or Effect.fail) inside onEvent or onAlarm surfaces as a TaskExecutionError with the original error as cause. The onError handler receives the typed error union EErr | AErr and can use the full TaskContext<S> to react:
const myTask = Task.define({
  state: MyState,
  event: MyEvent,
  onEvent: (_ctx, _event) => Effect.fail(new MyError("something broke")),
  onAlarm: () => Effect.void,
  onError: (ctx, error) =>
    Effect.gen(function* () {
      // error is typed as MyError here
      yield* ctx.save({ count: -1 })
    }),
})

Full example

import { Effect, Schema } from "effect"
import { Task } from "@durable-effect/task"
import { createTasks } from "@durable-effect/task/cloudflare"

const CounterState = Schema.Struct({
  count: Schema.Number,
})

const CounterEvent = Schema.Struct({
  _tag: Schema.Literal("Start"),
})

const counter = Task.define({
  state: CounterState,
  event: CounterEvent,

  onEvent: (ctx, _event) =>
    Effect.gen(function* () {
      yield* ctx.save({ count: 0 })
      yield* ctx.scheduleIn("2 seconds")
    }),

  onAlarm: (ctx) =>
    Effect.gen(function* () {
      const current = yield* ctx.recall()
      const count = (current?.count ?? 0) + 1
      yield* ctx.save({ count })

      if (count >= 10) {
        yield* ctx.purge() // delete all state and stop the task
      }

      yield* ctx.scheduleIn("2 seconds")
    }),

  onError: (ctx, _error) =>
    Effect.gen(function* () {
      yield* ctx.save({ count: -1 })
    }),

  onClientGetState: (_ctx, state) =>
    Effect.succeed(state === null ? null : { count: state.count }),
})

export const { TasksDO, tasks } = createTasks({ counter })

Build docs developers (and LLMs) love