Skip to main content
createTasks() is the main entry point for deploying tasks to Cloudflare Workers. It takes a record of TaskDefinition values and returns a Durable Object class to export from your worker plus a fully type-safe client accessor.

Import paths

The package exposes two entry points:
// Core — task definitions, schemas, testing utilities
import { Task, withServices } from "@durable-effect/task"

// Cloudflare — Durable Object factory and low-level adapters
import { createTasks, makeTaskEngine, makeCloudflareStorage, makeCloudflareAlarm } from "@durable-effect/task/cloudflare"

createTasks(definitions)

function createTasks<
  const T extends Record<string, TaskDefinition<any, any, any, any, never>>,
>(
  definitions: T,
): {
  TasksDO: typeof DurableObject
  tasks: TasksAccessor<T>
}
definitions
Record<string, TaskDefinition>
required
A record mapping task names to TaskDefinition values. Every definition must have R = never — use withServices() to eliminate service requirements before passing here.The keys become the task names used in tasks(doNamespace, name) and are checked at compile time.
const { TasksDO, tasks } = createTasks({
  counter,      // TaskDefinition<CounterState, CounterEvent, ...>
  emailer,      // TaskDefinition<EmailState, EmailEvent, ...>
  scheduler,    // TaskDefinition<SchedulerState, SchedulerEvent, ...>
})

Return value

TasksDO
class
A Durable Object class that implements fetch(request) and alarm(). Export this from your worker entry point so Cloudflare can instantiate it.All task definitions share a single DO class. Each unique combination of task name and instance ID gets its own isolated Durable Object with its own storage.
tasks
TasksAccessor<T>
A callable that returns a TaskHandle bound to a specific DO namespace and task name. The task name is checked against the keys of definitions at compile time.
const counter = tasks(env.TASKS_DO, "counter")
// counter is TaskHandle<CounterState, CounterEvent>

TasksAccessor<T>

interface TasksAccessor<T extends Record<string, TaskDefinition<...>>> {
  <K extends keyof T & string>(
    doNamespace: DurableObjectNamespaceLike,
    name: K,
  ): TaskHandle<StateOf<T, K>, EventOf<T, K>>
}
TasksAccessor is a generic callable. When you pass a task name K, TypeScript infers the correct S and E types from your definitions record, giving you a fully typed TaskHandle with no casts.
const c = tasks(env.TASKS_DO, "counter")    // TaskHandle<CounterState, CounterEvent>
const e = tasks(env.TASKS_DO, "emailer")    // TaskHandle<EmailState, EmailEvent>

// Compile error — "unknown" is not a key of the definitions record
const x = tasks(env.TASKS_DO, "unknown")

TaskHandle<S, E>

interface TaskHandle<S, E> {
  send(id: string, event: E): Effect<void, TaskClientError>
  getState(id: string): Effect<S | null, TaskClientError>
  alarm(id: string): Effect<void, TaskClientError>
}
send
(id: string, event: E) => Effect<void, TaskClientError>
Sends an event to a task instance. The id identifies the specific instance — multiple instances of the same task can run concurrently with different IDs.The event payload is type-checked against the task’s event schema at compile time.
yield* counter.send("my-counter-id", { _tag: "Start" }).pipe(Effect.orDie)
getState
(id: string) => Effect<S | null, TaskClientError>
Reads the current persisted state of a task instance. Returns null if no state has been saved yet.If the task defines an onClientGetState hook, the hook runs and its return value is what getState() returns — not the raw stored state.
const state = yield* counter.getState("my-counter-id").pipe(Effect.orDie)
// state is CounterState | null
alarm
(id: string) => Effect<void, TaskClientError>
Manually triggers the onAlarm handler for a task instance without waiting for a scheduled alarm. Useful for testing or for driving alarm logic imperatively.
yield* counter.alarm("my-counter-id").pipe(Effect.orDie)

Low-level API

These exports are for advanced setups where you need a custom Durable Object class instead of the generated one.

makeTaskEngine(state, config)

function makeTaskEngine(
  state: DurableObjectState,
  config: { name: string; tasks: TaskRegistryConfig },
): {
  fetch: (request: Request) => Promise<Response>
  alarm: () => Promise<void>
}
Creates the engine that powers the Durable Object. Use this when you need to extend the DO class manually or integrate with an existing DO:
import { makeTaskEngine } from "@durable-effect/task/cloudflare"
import { registerTask, buildRegistryLayer } from "@durable-effect/task"

class MyTaskDO {
  private engine: ReturnType<typeof makeTaskEngine>

  constructor(state: DurableObjectState) {
    this.engine = makeTaskEngine(state, {
      name: "counter",
      tasks: { counter: registerTask(counterTask) },
    })
  }

  fetch(request: Request) { return this.engine.fetch(request) }
  alarm() { return this.engine.alarm() }
}

makeCloudflareStorage(doStorage)

function makeCloudflareStorage(
  doStorage: DurableObjectStorageMethods,
): Layer<Storage>
Wraps the Cloudflare Durable Object storage API into the Storage Effect service layer. Pass state.storage from the DO constructor.
const storageLayer = makeCloudflareStorage(state.storage)

makeCloudflareAlarm(doStorage)

function makeCloudflareAlarm(
  doStorage: DurableObjectAlarmMethods,
): Layer<Alarm>
Wraps Cloudflare’s alarm methods (setAlarm, deleteAlarm, getAlarm) into the Alarm Effect service layer. Pass state.storage from the DO constructor — the alarm methods are on the same storage object.
const alarmLayer = makeCloudflareAlarm(state.storage)

Error types

TaskClientError
TaggedError
Returned by send(), getState(), and alarm() when the HTTP call to the Durable Object fails (network error, non-2xx response, etc.).
class TaskClientError extends Data.TaggedError("TaskClientError")<{
  readonly message: string
  readonly cause?: unknown
}> {}
TaskNotFoundError
TaggedError
Surfaced when a task name is not found in the registry — for example, when using the low-level makeTaskEngine API with an incorrect task name.
class TaskNotFoundError extends Data.TaggedError("TaskNotFoundError")<{
  readonly name: string
}> {}
TaskValidationError
TaggedError
Surfaced when an incoming event payload fails to decode against the task’s event schema.
class TaskValidationError extends Data.TaggedError("TaskValidationError")<{
  readonly message: string
  readonly cause?: unknown
}> {}
TaskExecutionError
TaggedError
Surfaced when onEvent or onAlarm fails and no onError handler is defined.
class TaskExecutionError extends Data.TaggedError("TaskExecutionError")<{
  readonly cause: unknown
}> {}

Full setup example

1

Define the task

// src/tasks/counter.ts
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()
      }

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

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

Export the Durable Object

// src/index.ts
export { TasksDO } from "./tasks/counter.js"

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    // your fetch handler
  },
} satisfies ExportedHandler<Env>
3

Configure wrangler

// wrangler.jsonc
{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_flags": ["nodejs_compat"],
  "durable_objects": {
    "bindings": [
      {
        "name": "TASKS_DO",
        "class_name": "TasksDO"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_classes": ["TasksDO"]
    }
  ]
}
4

Send events and read state

import { Effect } from "effect"
import { tasks } from "./tasks/counter.js"
import { env } from "cloudflare:workers"

const counter = tasks(env.TASKS_DO, "counter")

// Send an event — payload type is checked against CounterEvent schema
yield* counter.send("my-counter-id", { _tag: "Start" }).pipe(Effect.orDie)

// Read state — returns CounterState | null
const state = yield* counter.getState("my-counter-id").pipe(Effect.orDie)

Build docs developers (and LLMs) love