Skip to main content
Continuous jobs run on a repeating schedule. Each instance lives in its own Durable Object and maintains persistent state between runs.

Continuous.make(config)

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

const tokenRefresher = Continuous.make({
  stateSchema: Schema.Struct({
    accessToken: Schema.String,
    refreshToken: Schema.String,
    expiresAt: Schema.Number,
  }),
  schedule: Continuous.every("30 minutes"),
  execute: (ctx) =>
    Effect.gen(function* () {
      const state = yield* ctx.state;
      const newToken = yield* refreshAccessToken(state.refreshToken);
      yield* ctx.setState({
        ...state,
        accessToken: newToken,
        expiresAt: Date.now() + 1_800_000,
      });
    }),
});

ContinuousMakeConfig fields

stateSchema
Schema.Schema<S>
required
Effect Schema used to validate and serialize the job’s persisted state. Accepts any Effect Schema — Schema.Struct, Schema.Class, etc. State is validated on every read and write; invalid state throws ValidationError.
schedule
ContinuousSchedule
required
When to execute the job. Use Continuous.every(duration) for fixed intervals or Continuous.cron(expression, tz?) for cron-based schedules. See ContinuousSchedule below.
execute
(ctx: ContinuousContext<S>) => Effect<void, E, never>
required
The function to run on each scheduled execution. Must return Effect<void, E, never> — all service requirements must be satisfied before passing to Continuous.make. If your effect needs services, provide them via .pipe(Effect.provide(layer)).
startImmediately
boolean
default:"true"
When true, the job executes once immediately on start before waiting for the first scheduled interval. Set to false to skip the initial run.
retry
RetryConfig
Automatic retry configuration for execute failures.
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

ContinuousSchedule

Two schedule types are supported.

Continuous.every(duration)

Executes the job at a fixed interval measured from the end of the previous run.
Continuous.every("30 minutes")
Continuous.every("1 hour")
Continuous.every(Duration.hours(6))
The duration argument accepts anything that Effect’s Duration.DurationInput accepts — a string such as "5 minutes", a Duration value, or milliseconds as a number.

Continuous.cron(expression, tz?)

Executes the job according to a 6-field cron expression (seconds, minutes, hours, days, months, weekdays). Uses Effect’s built-in Cron module for parsing.
// Every day at 4:00 AM UTC
Continuous.cron("0 0 4 * * *")

// Every Monday at 9:00 AM New York time
Continuous.cron("0 0 9 * * 1", "America/New_York")
expression
string
required
A 6-field cron expression: seconds minutes hours days months weekdays.
tz
string
Optional IANA timezone name (e.g., "America/New_York", "UTC"). Defaults to UTC.

ContinuousContext<S>

The context object passed to your execute function on every scheduled run.
PropertyTypeDescription
stateEffect<S>Yields the current persisted state
setState(s)(s: S) => Effect<void>Replaces the entire state
updateState(fn)(fn: (s: S) => S) => Effect<void>Transforms state via a function
terminate(opts?)(opts?: TerminateOptions) => Effect<never>Stops the job and purges all state
instanceIdstringFull Durable Object instance ID
jobNamestringRegistered job name
runCountnumberExecution count (1-indexed)
attemptnumberCurrent retry attempt (1 = first try)
isRetrybooleantrue when this is a retry of a previous failure
state
Effect<S>
Lazily loads the current state from Durable Object storage. Yield it to get the value:
const s = yield* ctx.state;
setState
(s: S) => Effect<void>
Persists a new state value, completely replacing the current one. The value is validated against stateSchema before writing.
updateState
(fn: (current: S) => S) => Effect<void>
Reads the current state, applies fn, and writes the result back. More concise than calling setState and state separately.
yield* ctx.updateState((s) => ({ ...s, count: s.count + 1 }));
terminate
(options?: TerminateOptions) => Effect<never>
Immediately stops the job: cancels any pending alarm, deletes all state from storage, and short-circuits the current execution. No code after the yield* runs.
if (state.consecutiveFailures > 10) {
  return yield* ctx.terminate({ reason: "Too many failures" });
}
instanceId
string
The Durable Object instance ID in the format continuous:{jobName}:{userProvidedId}.
jobName
string
The name of the job as registered — matches the key used in createDurableJobs.
runCount
number
How many times execute has been called for this instance. 1 on the very first run.
attempt
number
The retry attempt number within a single scheduled execution. 1 means the first attempt, 2 means the first retry, and so on. Only meaningful when retry is configured.
isRetry
boolean
Convenience flag — true when attempt > 1. Use this to add retry-specific behavior without checking attempt directly.

TerminateOptions

reason
string
An optional human-readable reason for termination. Included in the job.terminated telemetry event and returned in the terminate response.

Full example

import { Effect, Schema } from "effect";
import { Continuous, createDurableJobs } 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" }),
    jitter: true,
  },

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

      if (ctx.isRetry) {
        yield* Effect.log(`Retry attempt ${ctx.attempt} for run #${ctx.runCount}`);
      }

      const healthy = yield* checkHealth();

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

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

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

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

    await Effect.runPromise(
      client.continuous("healthChecker").start({
        id: "primary-api",
        input: { lastCheckAt: 0, consecutiveFailures: 0 },
      })
    );

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

Build docs developers (and LLMs) love