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
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.
When true, the job executes once immediately on start before entering the schedule.
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.
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.
| Property | Type | Description |
|---|
state | Effect<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. |
instanceId | string | Unique Durable Object instance ID. |
jobName | string | Job name as registered in createDurableJobs. |
runCount | number | How many times execute has been called (1-indexed). |
attempt | number | Current retry attempt (1 = first try, 2+ = retry). |
isRetry | boolean | Whether 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 };