Skip to main content
Task jobs are event-driven state machines where you control the full lifecycle. Events update state and optionally schedule execution, while execute runs when the scheduled alarm fires. Unlike Continuous, there is no automatic schedule — you decide when (and whether) to run.
This is the Task job type from @durable-effect/jobs. It is distinct from the @durable-effect/task package.

Task.make(config)

Creates an unregistered task job definition. The job name is assigned later via the key you use in createDurableJobs.
import { Task } from "@durable-effect/jobs";
import { Schema, Effect, Duration } from "effect";

const orderProcessor = Task.make({
  stateSchema: Schema.Struct({
    orderId: Schema.String,
    status: Schema.Literal("pending", "processing", "shipped", "delivered"),
    attempts: Schema.Number,
  }),
  eventSchema: Schema.Union(
    Schema.Struct({ _tag: Schema.Literal("OrderPlaced"), orderId: Schema.String }),
    Schema.Struct({ _tag: Schema.Literal("PaymentReceived") }),
    Schema.Struct({ _tag: Schema.Literal("Shipped"), trackingNumber: Schema.String }),
  ),
  onEvent: (event, ctx) =>
    Effect.gen(function* () {
      switch (event._tag) {
        case "OrderPlaced":
          yield* ctx.setState({ orderId: event.orderId, status: "pending", attempts: 0 });
          yield* ctx.schedule(Duration.minutes(5));
          break;
        case "PaymentReceived":
          yield* ctx.updateState((s) => ({ ...s, status: "processing" }));
          break;
        case "Shipped":
          yield* ctx.updateState((s) => ({ ...s, status: "shipped" }));
          yield* ctx.schedule(Duration.hours(24));
          break;
      }
    }),
  execute: (ctx) =>
    Effect.gen(function* () {
      const state = yield* ctx.state;
      if (!state) return;
      if (state.status === "pending") {
        yield* sendPaymentReminder(state.orderId);
        yield* ctx.schedule(Duration.minutes(30));
      }
    }),
});

TaskMakeConfig fields

stateSchema
Schema.Schema<S>
required
Effect Schema for validating and serializing the job’s persisted state. State is durably stored in the Durable Object and survives restarts. Invalid state throws ValidationError.
eventSchema
Schema.Schema<E>
required
Effect Schema for validating incoming events. Every event sent via .send() is decoded against this schema before being passed to onEvent.
onEvent
(event: E, ctx: TaskEventContext<S>) => Effect<void, Err, never>
required
Called for each incoming event. The validated event is passed as the first parameter — a plain value, not an Effect. The context provides state access, scheduling, and metadata.Typical responsibilities:
  • Update state based on the event via ctx.setState or ctx.updateState
  • Schedule the next execution via ctx.schedule if needed
  • Optionally terminate the task via ctx.terminate
execute
(ctx: TaskExecuteContext<S>) => Effect<void, Err, never>
required
Called when the durable alarm fires. The context provides the same state access and scheduling capabilities as onEvent.Typical responsibilities:
  • Process the current state
  • Schedule the next execution if more work remains
  • Terminate the task via ctx.terminate when work is complete
onIdle
(ctx: TaskExecuteContext<S>) => Effect<void, never, never>
Called when either onEvent or execute completes and no alarm is scheduled. Use this to schedule delayed cleanup, log idle state, or trigger maintenance tasks. Must return Effect<void, never, never>.
onIdle: (ctx) =>
  Effect.gen(function* () {
    // Schedule cleanup in 1 hour if nothing happens
    yield* ctx.schedule(Duration.hours(1));
  }),
onError
(error: Err, ctx: TaskErrorContext<S>) => Effect<void, never, never>
Optional error handler called when onEvent or execute throws. If not provided, errors are logged and the task continues. Must return Effect<void, never, never>.Use this to log errors, track failure counts in state, schedule retries, or terminate on fatal errors.
onError: (error, ctx) =>
  Effect.gen(function* () {
    yield* Effect.logError("Task failed", error);
    yield* ctx.updateState((s) => ({ ...s, attempts: s.attempts + 1 }));
    yield* ctx.schedule(Duration.seconds(30));
  }),
logging
boolean | LogLevel
default:"false"
Controls the log level for this job’s internal logs.
ValueBehavior
false (default)LogLevel.Error — only failures
trueLogLevel.Debug — all logs
LogLevel.WarningWarnings and above
LogLevel.NoneSilent

TaskEventContext<S>

The context passed to onEvent for each incoming event.
PropertyTypeDescription
stateEffect<S | null>Current state (null if no state set yet)
setState(s)(s: S) => Effect<void>Replace entire state
updateState(fn)(fn: (s: S) => S) => Effect<void>Transform state (no-op if state is null)
schedule(when)(when) => Effect<void>Schedule execution
cancelSchedule()() => Effect<void>Cancel scheduled execution
getScheduledTime()() => Effect<number | null>Get scheduled time (ms) or null
terminate()() => Effect<never>Terminate the task and purge all state
instanceIdstringDurable Object instance ID
jobNamestringRegistered job name
executionStartedAtnumberTimestamp (ms) when this handler started
isFirstEventbooleantrue if state was null before this event
eventCountEffect<number>Total events received
createdAtEffect<number>When this task was created (ms)
state
Effect<S | null>
Lazily loads state from storage. Returns null if no state has been set yet — use isFirstEvent to detect the first event without an extra yield.
const state = yield* ctx.state;
if (state === null) {
  yield* ctx.setState({ /* initial */ });
}
setState
(s: S) => Effect<void>
Persists a new state value, replacing the current one entirely.
updateState
(fn: (current: S) => S) => Effect<void>
Reads state, applies fn, and writes the result back. No-op if state is currently null.
schedule
(when: Duration | string | number | Date) => Effect<void>
Schedules the next execute call. Accepts flexible time inputs — see Schedule inputs below.
cancelSchedule
() => Effect<void>
Cancels any pending scheduled execution. Has no effect if no alarm is set.
getScheduledTime
() => Effect<number | null>
Returns the scheduled execution time as a Unix timestamp in milliseconds, or null if no alarm is set.
terminate
() => Effect<never>
Cancels any pending alarm, deletes all state from storage, and short-circuits the current handler. No code after the yield* runs.
isFirstEvent
boolean
true when the state was null before this event arrived. Use this to initialize state on the first event without an extra yield* ctx.state check.
eventCount
Effect<number>
Total number of events received by this task instance.
createdAt
Effect<number>
Unix timestamp in milliseconds of when this task instance was first created.

TaskExecuteContext<S>

The context passed to execute (and onIdle) when the alarm fires.
PropertyTypeDescription
stateEffect<S | null>Current state (null if not set)
setState(s)(s: S) => Effect<void>Replace entire state
updateState(fn)(fn: (s: S) => S) => Effect<void>Transform state
schedule(when)(when) => Effect<void>Schedule next execution
cancelSchedule()() => Effect<void>Cancel scheduled execution
getScheduledTime()() => Effect<number | null>Get scheduled time (ms)
terminate()() => Effect<never>Terminate task and purge all state
instanceIdstringDurable Object instance ID
jobNamestringRegistered job name
executionStartedAtnumberTimestamp (ms) when this handler started
eventCountEffect<number>Total events received
executeCountEffect<number>Number of times execute has run (1-indexed)
createdAtEffect<number>When this task was created (ms)
All scheduling and state fields behave identically to TaskEventContext<S>. The additional fields are:
executeCount
Effect<number>
How many times execute has been called for this instance. 1 on the first execution.

Schedule inputs

ctx.schedule(when) accepts four forms:
// Effect Duration (from now)
yield* ctx.schedule(Duration.seconds(30));
yield* ctx.schedule(Duration.hours(24));

// Duration string (from now)
yield* ctx.schedule("5 minutes");

// Absolute timestamp in milliseconds
yield* ctx.schedule(Date.now() + 60_000);

// Date object
yield* ctx.schedule(new Date("2024-12-31T00:00:00Z"));

Full example

import { Effect, Schema, Duration } from "effect";
import { Task, createDurableJobs } from "@durable-effect/jobs";

const orderProcessor = Task.make({
  stateSchema: Schema.Struct({
    orderId: Schema.String,
    status: Schema.Literal("pending", "processing", "shipped", "delivered"),
    attempts: Schema.Number,
  }),

  eventSchema: Schema.Union(
    Schema.Struct({ _tag: Schema.Literal("OrderPlaced"), orderId: Schema.String }),
    Schema.Struct({ _tag: Schema.Literal("PaymentReceived") }),
    Schema.Struct({ _tag: Schema.Literal("Shipped"), trackingNumber: Schema.String }),
  ),

  onEvent: (event, ctx) =>
    Effect.gen(function* () {
      switch (event._tag) {
        case "OrderPlaced":
          yield* ctx.setState({
            orderId: event.orderId,
            status: "pending",
            attempts: 0,
          });
          // Check payment in 5 minutes
          yield* ctx.schedule(Duration.minutes(5));
          break;

        case "PaymentReceived":
          yield* ctx.updateState((s) => ({ ...s, status: "processing" }));
          break;

        case "Shipped":
          yield* ctx.updateState((s) => ({ ...s, status: "shipped" }));
          // Check delivery in 24 hours
          yield* ctx.schedule(Duration.hours(24));
          break;
      }
    }),

  execute: (ctx) =>
    Effect.gen(function* () {
      const state = yield* ctx.state;
      if (!state) return;

      if (state.status === "pending") {
        yield* sendPaymentReminder(state.orderId);
        yield* ctx.schedule(Duration.minutes(30));
      }

      if (state.status === "shipped") {
        const delivered = yield* checkDelivery(state.orderId);
        if (delivered) {
          yield* ctx.updateState((s) => ({ ...s, status: "delivered" }));
          yield* ctx.terminate(); // Order complete
        } else {
          yield* ctx.schedule(Duration.hours(24)); // Check again tomorrow
        }
      }
    }),

  onIdle: (ctx) =>
    Effect.gen(function* () {
      // Schedule cleanup in 1 hour if nothing is pending
      yield* ctx.schedule(Duration.hours(1));
    }),

  onError: (error, ctx) =>
    Effect.gen(function* () {
      yield* Effect.logError("Task failed", error);
      yield* ctx.updateState((s) => ({ ...s, attempts: s.attempts + 1 }));
      yield* ctx.schedule(Duration.seconds(30));
    }),
});

const { Jobs, JobsClient } = createDurableJobs({ orderProcessor });
export { Jobs };

export default {
  async fetch(request: Request, env: Env) {
    const client = JobsClient.fromBinding(env.JOBS);

    // Send an event — creates the instance if it doesn't exist
    const result = await Effect.runPromise(
      client.task("orderProcessor").send({
        id: "order-789",
        event: { _tag: "OrderPlaced", orderId: "order-789" },
      })
    );

    // result.created      — true if this was the first event
    // result.instanceId   — full DO instance ID
    // result.scheduledAt  — timestamp of next execution (null if none)

    return new Response("OK");
  },
};

Build docs developers (and LLMs) love