Skip to main content

Workflows

Workflows provide durable execution so you can write programs that are resilient to any failure. Workflows are comprised of steps, which wrap ordinary Java methods. If a workflow is interrupted for any reason (e.g., an executor restarts or crashes), when your program restarts the workflow automatically resumes execution from the last completed step.

To write a workflow, annotate a method with @Workflow. All workflow methods must be registered before DBOS is launched. A workflow method can have any parameters and return type (including void), as long as they are serializable.

Here's an example of a workflow:

interface Example {
public String workflow();
}

class ExampleImpl implements Example {
private final DBOS dbos;

public ExampleImpl(DBOS dbos) {
this.dbos = dbos;
}

private void stepOne() {
System.out.println("Step one completed!");
}

private void stepTwo() {
System.out.println("Step two completed!");
}

@Override
@Workflow
public String workflow() {
dbos.runStep(() -> stepOne(), "stepOne");
dbos.runStep(() -> stepTwo(), "stepTwo");
return "success";
}
}

public class App {
public static void main(String[] args) throws Exception {
// Configure and create a DBOS instance
DBOSConfig config = ...
DBOS dbos = new DBOS(config);

// Register the workflow, creating a proxy object
Example proxy = dbos.registerProxy(Example.class, new ExampleImpl(dbos));

// Launch DBOS after registering all workflows
dbos.launch();

// Call the registered workflow through the proxy
String result = proxy.workflow();
System.out.println("Workflow result: " + result);
}
}

Starting Workflows In The Background

One common use-case for workflows is building reliable background tasks that keep running even when your program is interrupted, restarted, or crashes. You can use startWorkflow to start a workflow in the background. When you start a workflow this way, it returns a workflow handle, from which you can access information about the workflow or wait for it to complete and retrieve its result.

Here's an example:

public void runWorkflowExample(DBOS dbos, Example proxy) throws Exception {
// Start the background task
WorkflowHandle<String, Exception> handle = dbos.startWorkflow(() -> proxy.workflow());

// Wait for the background task to complete and retrieve its result
String result = handle.getResult();
System.out.println("Workflow result: " + result);
}

After starting a workflow in the background, you can use retrieveWorkflow to retrieve a workflow's handle from its ID. You can also retrieve a workflow's handle from outside of your DBOS application with DBOSClient.retrieveWorkflow.

If you need to run many workflows in the background and manage their concurrency or flow control, use queues.

Workflow IDs and Idempotency

Every time you execute a workflow, that execution is assigned a unique ID, by default a UUID. You can access this ID from the DBOS.workflowId method. Workflow IDs are useful for communicating with workflows and developing interactive workflows.

You can set the workflow ID of a workflow using withWorkflowId method of WorkflowOptions or StartWorkflowOptions. Workflow IDs are globally unique within your application. An assigned workflow ID acts as an idempotency key: if a workflow is called multiple times with the same ID, it executes only once. This is useful if your operations have side effects like making a payment or sending an email. For example:

public void directInvocationExample(DBOS dbos, Example proxy) throws Exception {
String myID = "unique-workflow-id-123";
WorkflowOptions options = new WorkflowOptions().withWorkflowId(myID);
try (var _ctx = new options.setContext()) {
var result = proxy.workflow();
System.out.println("Result: " + result);
}
}

public void startWorkflowExample(DBOS dbos, Example proxy) throws Exception {
String myID = "unique-workflow-id-123";
WorkflowHandle<String, RuntimeException> handle = dbos.startWorkflow(
() -> proxy.exampleWorkflow(),
new StartWorkflowOptions().withWorkflowId(myID)
);
String result = handle.getResult();
System.out.println("Result: " + result);
}

Determinism

Workflows are in most respects normal Java methods. They can have loops, branches, conditionals, and so on. However, a workflow method must be deterministic: if called multiple times with the same inputs, it should invoke the same steps with the same inputs in the same order (given the same return values from those steps). If you need to perform a non-deterministic operation like accessing the database, calling a third-party API, generating a random number, or getting the local time, you shouldn't do it directly in a workflow method. Instead, you should do all non-deterministic operations in steps.

warning

Java's threading and concurrency APIs are non-deterministic. You should use them only inside steps.

For example, don't do this:

@Workflow
public String workflow() {
// Random number generation is not deterministic!
// This workflow is not idempotent!
int randomChoice = new Random().nextInt(2);
if (randomChoice == 0) {
return dbos.runStep(() -> stepOne(), "stepOne");
} else {
return dbos.runStep(() -> stepTwo(), "stepTwo");
}
}

Instead, do this:

private int generateChoice() {
return new Random().nextInt(2);
}

@Workflow
public String workflow() {
// this workflow is idempotent because the random number generation
// is inside a step so it only gets executed once per workflow ID
int randomChoice = dbos.runStep(() -> generateChoice(), "generateChoice");
if (randomChoice == 0) {
return dbos.runStep(() -> stepOne(), "stepOne");
} else {
return dbos.runStep(() -> stepTwo(), "stepTwo");
}
}

Workflow Timeouts

You can set a timeout for a workflow using withTimeout in WorkflowOptions and StartWorkflowOptions.

When the timeout expires, the workflow and all its children (by default) are cancelled. Cancelling a workflow sets its status to CANCELLED and preempts its execution at the beginning of its next step. You can detach a child workflow from its parent's timeout by starting it with a custom timeout using withTimeout.

Timeouts are start-to-completion: if a workflow is enqueued, the timeout does not begin until the workflow is dequeued and starts execution. Also, timeouts are durable: they are stored in the database and persist across restarts, so workflows can have very long timeouts.

// set timeout for direct invocation
var options = new WorkflowOptions().withTimeout(Duration.ofHours(12));
try (var _ctx = options.setContext()) {
proxy.workflow();
}

// set timeout with start workflow
var handle = dbos.startWorkflow(
() -> proxy.workflow(),
new StartWorkflowOptions().withTimeout(Duration.ofHours(12))
);

Durable Sleep

You can use sleep to put your workflow to sleep for any period of time. This sleep is durable—DBOS saves the wakeup time in the database so that even if the workflow is interrupted and restarted multiple times while sleeping, it still wakes up on schedule.

Sleeping is useful for scheduling work to run in the future (even days, weeks, or months from now). For example:

public String runTask(String task) {
// Execute the task...
return "task completed";
}

@Workflow
public String exampleWorkflow(Duration sleepTime, String task) {
// Sleep for the specified duration
dbos.sleep(sleepTime);

// Execute the task after sleeping
String result = dbos.runStep(
() -> runTask(task),
"runTask"
);

return result;
}

Workflow Guarantees

Workflows provide the following reliability guarantees. These guarantees assume that the application and database may crash and go offline at any point in time, but are always restarted and return online.

  1. Workflows always run to completion. If a DBOS process is interrupted while executing a workflow and restarts, it resumes the workflow from the last completed step.
  2. Steps are tried at least once but are never re-executed after they complete. If a failure occurs inside a step, the step may be retried, but once a step has completed (returned a value or thrown an exception to the calling workflow), it will never be re-executed.

If an exception is thrown from a workflow, the workflow terminates—DBOS records the exception, sets the workflow status to ERROR, and does not recover the workflow. This is because uncaught exceptions are assumed to be nonrecoverable. If your workflow performs operations that may transiently fail (for example, sending HTTP requests to unreliable services), those should be performed in steps with configured retries. DBOS provides tooling to help you identify failed workflows and examine the specific uncaught exceptions.