Skip to main content
A Continuous job runs a handler function on a repeating schedule. Each instance runs in its own Durable Object and maintains persistent state between executions. Use Continuous jobs for background work that needs to happen repeatedly: token refresh, health checks, daily reports, and similar patterns.

Defining a continuous job

import { Effect, Schema } from "effect";
import { Continuous } from "@durable-effect/jobs";
import { Backoff } from "@durable-effect/core";

const healthChecker = Continuous.make({
  stateSchema: Schema.Struct({
    lastCheckAt: Schema.Number,
    consecutiveFailures: Schema.Number,
  }),

  schedule: Continuous.every("5 minutes"),

  startImmediately: true,

  retry: {
    maxAttempts: 3,
    delay: Backoff.exponential({ base: "1 second", max: "30 seconds" }),
  },

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

      console.log(`Run #${ctx.runCount}, attempt ${ctx.attempt}`);

      const healthy = yield* checkHealth();
      if (!healthy && state.consecutiveFailures > 10) {
        return yield* ctx.terminate({ reason: "Too many failures" });
      }

      yield* ctx.updateState((s) => ({
        lastCheckAt: Date.now(),
        consecutiveFailures: healthy ? 0 : s.consecutiveFailures + 1,
      }));
    }),
});

Configuration

stateSchema
Schema.Schema<S>
required
Effect Schema for validating and serializing the job’s persistent state. State is validated on every read and write — invalid state throws ValidationError.
schedule
ContinuousSchedule
required
When to execute. Use Continuous.every() for fixed intervals or Continuous.cron() for cron expressions.
execute
(ctx: ContinuousContext<S>) => Effect<void, E, never>
required
The function to run on each scheduled tick. Must return Effect<void, E, never> — all service requirements must be provided via Effect.provide.
startImmediately
boolean
default:"true"
When true, the job executes once immediately on start before entering the schedule.
retry
JobRetryConfig
Optional retry configuration for execute failures. When configured, failed executions are retried up to maxAttempts times. Retries are scheduled via durable alarms. When all retries are exhausted, the job is terminated and a job.retryExhausted event is emitted.
logging
boolean | LogLevel
Control logging verbosity for this job. false (default) logs only errors. true enables all logs (debug level). Pass a LogLevel value for custom control.

Schedule options

Continuous.every(interval)

Execute at a fixed interval. Accepts any Duration.DurationInput.
Continuous.every("30 minutes")
Continuous.every("1 hour")
Continuous.every(Duration.hours(6))

Continuous.cron(expression, tz?)

Execute on a cron schedule. Uses a 6-field cron format: seconds minutes hours days months weekdays.
// Every day at 4am UTC
Continuous.cron("0 0 4 * * *")

// Every Monday at 9am New York time
Continuous.cron("0 0 9 * * 1", "America/New_York")
The cron format used here is 6 fields (including seconds), not the traditional 5-field format.

Execution context

The ctx object passed to execute provides state access, metadata, and lifecycle control.
PropertyTypeDescription
stateEffect<S>Current state value. Yield to read.
setState(s)Effect<void>Replace the entire state.
updateState(fn)Effect<void>Transform state with a function.
terminate(opts?)Effect<never>Stop the job and purge all state. Short-circuits execution.
instanceIdstringUnique Durable Object instance ID.
jobNamestringJob name as registered in createDurableJobs.
runCountnumberHow many times execute has been called (1-indexed).
attemptnumberCurrent retry attempt (1 = first try, 2+ = retry).
isRetrybooleanWhether this execution is a retry of a previous failure.

Retry configuration

import { Backoff } from "@durable-effect/core";

const job = Continuous.make({
  // ...
  retry: {
    maxAttempts: 3,
    delay: Backoff.exponential({
      base: "1 second",
      max: "30 seconds",
    }),
    jitter: true, // Add randomness to prevent thundering herd
  },
  execute: (ctx) =>
    Effect.gen(function* () {
      if (ctx.isRetry) {
        console.log(`Retry attempt ${ctx.attempt}`);
      }
      // ... your logic
    }),
});
Backoff strategies:
// Exponential: 1s, 2s, 4s, 8s... (capped at max)
Backoff.exponential({ base: "1 second", max: "30 seconds" })

// Linear: 1s, 2s, 3s, 4s...
Backoff.linear({ initial: "1 second", increment: "1 second" })

// Fixed: always 5s
Backoff.constant("5 seconds")

Terminating a job

Call ctx.terminate() from within execute to permanently stop the job. Terminating:
  • Cancels any scheduled alarm
  • Deletes all state from storage
  • Short-circuits the current execution — no code after the call runs
execute: (ctx) =>
  Effect.gen(function* () {
    const state = yield* ctx.state;

    if (state.consecutiveFailures > 10) {
      // Removes all state and cancels the schedule
      return yield* ctx.terminate({ reason: "Too many failures" });
    }

    // ... rest of logic only runs if not terminated
  }),
You can also terminate from the client: see Continuous client methods.

Logging

Control log verbosity per job:
import { LogLevel } from "effect";

Continuous.make({
  // ...
  logging: false,           // LogLevel.Error (failures only) — DEFAULT
  logging: true,            // LogLevel.Debug (all logs)
  logging: LogLevel.Warning,
  logging: LogLevel.None,   // Silent
});

Complete example

import { Effect, Schema } from "effect";
import { Continuous } from "@durable-effect/jobs";
import { Backoff } from "@durable-effect/core";
import { createDurableJobs } from "@durable-effect/jobs";

const healthChecker = Continuous.make({
  stateSchema: Schema.Struct({
    lastCheckAt: Schema.Number,
    consecutiveFailures: Schema.Number,
  }),

  schedule: Continuous.every("5 minutes"),
  startImmediately: true,

  retry: {
    maxAttempts: 3,
    delay: Backoff.exponential({ base: "1 second", max: "30 seconds" }),
  },

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

      console.log(`Run #${ctx.runCount}, attempt ${ctx.attempt}`);

      const healthy = yield* checkHealth();
      if (!healthy && state.consecutiveFailures > 10) {
        return yield* ctx.terminate({ reason: "Too many failures" });
      }

      yield* ctx.updateState((s) => ({
        lastCheckAt: Date.now(),
        consecutiveFailures: healthy ? 0 : s.consecutiveFailures + 1,
      }));
    }),
});

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

Build docs developers (and LLMs) love