Skip to main content
Version: 1.0

Checkpointed Tasks

A checkpointed task persists intermediate results while a Task Worker executes a TaskRun. If the task fails and is retried, the worker can reuse those results instead of repeating work that has already been checkpointed.

Checkpointing is useful when a single task performs several operations, such as calling multiple APIs or invoking tools in an agent loop. It provides lightweight durable execution inside a task without turning each operation into a separate workflow node.

Availability

Checkpointed tasks were introduced in LittleHorse 0.15.0. The Java checkpoint logging API used in the example below is available starting in 0.16.0.

The runnable examples below use SDK version 1.3.0 and require a compatible LittleHorse Server.

Concepts​

A Checkpoint stores a result associated with a TaskRun. Its ID contains the TaskRunId and a checkpoint number, starting at zero. Checkpoints belong to the TaskRun, so their results can be reused across its retry attempts.

Execution and Replay​

The Java, Python, Go, TypeScript, and .NET Task Worker SDKs provide a checkpoint method on the worker context. In Java, WorkerContext.executeAndCheckpoint() accepts a callback and its result type. Each SDK's checkpoint method does one of two things:

  1. If a prior attempt persisted a checkpoint at this position, the SDK retrieves and deserializes its value without executing the callback.
  2. Otherwise, the SDK executes the callback, persists its return value as a new checkpoint, and returns that value to the task method.

On retry, the task method starts again from the beginning. It does not resume at a saved line of code. Code outside checkpoint callbacks runs again, while callbacks with persisted results are skipped.

For example, if a task saves a checkpoint and then fails later in the task method, the next attempt reuses the saved result without executing that checkpoint callback again.

info

Checkpointing does not configure retries. Configure retries on the task node in your WfSpec, just as you would for any other task. See Exception Handling.

Replay-Safe Task Logic​

Checkpoints are matched by the order of checkpoint calls, not by callback name or arguments. Keep that order consistent across attempts of the same TaskRun.

  • Put work that should be skipped on replay inside the callback, rather than computing it before calling the checkpoint method.
  • Return the data needed by later code. On replay, the SDK restores the return value, not changes the callback made to in-memory state.
  • Use result types that the SDK can serialize and deserialize.
  • Keep branching and loop behavior consistent when replaying existing checkpoints. A branch based on a fresh random value or an external read can change which callback receives a saved result.
  • Take care when deploying changes to workers that may retry existing tasks. Inserting, removing, or reordering checkpoint calls, or changing their result types, can make existing checkpoints incompatible with the new code.

Checkpoints and Idempotency​

Checkpointing reduces duplicate work, but does not guarantee exactly-once execution of external side effects. An API call can succeed and the worker can fail before its result is persisted. A retry will then execute that callback again.

In Practice​

Define the Task Method​

The following task simulates sending a greeting and checkpoints a receipt. It deliberately fails after the checkpoint on attempt zero. On retry, it restores the receipt without executing the callback again. The console print represents the greeting operation; this example does not call an external service.

import io.littlehorse.sdk.worker.LHTaskMethod;
import io.littlehorse.sdk.worker.WorkerContext;

public class CheckpointTasksWorker {

@LHTaskMethod("checkpointed-side-effect")
public String checkpointedSideEffect(String name, WorkerContext context) {
int attemptNumber = context.getAttemptNumber();
System.out.printf(
"metadata: wfRun=%s taskRun=%s attempt=%d%n",
context.getWfRunId(),
context.getTaskRunId().getTaskGuid(),
attemptNumber);

String sideEffectReceipt = context.executeAndCheckpoint(
checkpointContext -> {
checkpointContext.log("sending the greeting side effect");
System.out.println("SIDE EFFECT: send greeting for " + name);
return "receipt-for-" + name;
},
String.class);

System.out.println("after checkpoint on attempt " + attemptNumber);
if (attemptNumber == 0) {
throw new RuntimeException("Intentional post-checkpoint failure on the first attempt.");
}

return sideEffectReceipt + "; completed on attempt " + attemptNumber;
}
}

The SDK supplies the WorkerContext to the task method. String.class tells the SDK how to deserialize each saved result. Logs written with checkpointContext.log() are persisted with the checkpoint.

Register and start the worker as described in Tasks and Task Workers.

Configure Retries in the Workflow​

Inside your workflow definition, schedule the task with retries:

WfRunVariable name = wf.declareStr("name").required();
wf.execute("checkpointed-side-effect", name).withRetries(2);

In Java, WfRunVariable is from io.littlehorse.sdk.wfsdk.

Here, wf is the workflow's WorkflowThread. Each linked runnable example includes the complete workflow and worker setup.

Observe Replay​

Start the worker using the command in your language's README, then run:

lhctl run checkpoint-tasks name obiwan

Look for these lines in the worker terminal:

SIDE EFFECT: send greeting for obiwan
after checkpoint on attempt 0
after checkpoint on attempt 1

On attempt zero, the callback prints SIDE EFFECT, saves receipt-for-obiwan as checkpoint zero, and the task then fails intentionally. On attempt one, the SDK retrieves that receipt without executing the callback. The after checkpoint line is outside the callback, so it prints on both attempts.

The successful task output is receipt-for-obiwan; completed on attempt 1. There is only one checkpoint. The workflow ends after the task, the receipt remains available in the TaskRun output.

Checkpoints or Workflow Nodes?​

Use workflow nodes to represent the steps and decisions in your business process. For example, verifying a customer's identity, approving an application, and notifying the customer are distinct business steps that belong in the WfSpec.

Use checkpoints for the technical work needed to carry out one of those steps. Within an identity-verification task, for example, checkpoints can persist results from individual API calls so that retries do not repeat calls whose results have already been saved. The workflow still sees one task node and consumes the task's final output; checkpointing does not add nodes to the WfSpec.

This keeps the business process visible in the workflow while leaving implementation details inside the Task Worker. Business-level waits, such as waiting for an external verification result, belong in the workflow using External Events or other workflow primitives, rather than keeping a Task Worker occupied.

API Reference​

The Task Worker SDK manages checkpoint persistence and retrieval. The underlying API includes: