Workflows
In this guide, you'll learn how to make your applications reliable using workflows.
Workflows orchestrate the execution of other functions, like transactions and steps. Workflows are reliable: if their execution is interrupted for any reason (e.g., an executor is restarted or crashes), DBOS automatically resumes them from where they left off, running them to completion without re-executing any operation that already finished. You can use workflows to coordinate multiple operations that must all complete for a program to be correct. For example, in our e-commerce demo, we use a workflow for payment processing.
Workflows must be annotated with the @Workflow
decorator and must have a WorkflowContext
as their first argument.
Like for other functions, inputs and outputs must be serializable to JSON.
Additionally, workflows must be deterministic.
Here's an example workflow from the programming guide. It signs an online guestbook then records the signature in the database. By using a workflow, we guarantee that every guestbook signature is recorded in the database, even if execution is interrupted.
class Greetings {
// Other function implementations
@Workflow()
@GetApi("/greeting/:friend")
static async Greeting(ctxt: HandlerContext, friend: string) {
const noteContent = `Thank you for being awesome, ${friend}!`;
await ctxt.invoke(Greetings).SignGuestbook(friend);
await ctxt.invoke(Greetings).InsertGreeting(
{ name: friend, note: noteContent }
);
return noteContent;
}
}
Invoking Functions from Workflows
Workflows can invoke transactions and steps using their ctxt.invoke()
method.
For example, this line from our above example invokes the transaction InsertGreeting
:
await ctxt.invoke(Greetings).InsertGreeting(friend, noteContent);
The syntax for invoking function fn(args)
in class Cls
is ctxt.invoke(Cls).fn(args)
.
You can also invoke other workflows with the ctxt.invokeWorkflow()
method.
The syntax for invoking workflow wf
in class Cls
with argument arg
is:
const output = await ctxt.invokeWorkflow(Cls).wf(arg);
Reliability 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.
- Workflows always run to completion. If a DBOS process crashes while executing a workflow and is restarted, it resumes the workflow from where it left off.
- Transactions commit exactly once. Once a workflow commits a transaction, it will never retry that transaction.
- Steps are tried at least once but are never re-executed after they successfully complete. If a failure occurs inside a step, the step may be retried, but once a step has completed, it will never be re-executed.
For safety, DBOS automatically attempts to recover a workflow a set number of times.
If a workflow exceeds this limit, its status is set to RETRIES_EXCEEDED
and it is no longer retried automatically, though it may be retried manually.
This acts as a dead letter queue so that a buggy workflow that crashes its application (for example, by running it out of memory) is not retried infinitely.
The maximum number of retries is by default 50, but this may be configured through arguments to the @Workflow
decorator.
Determinism
A workflow implementation must be deterministic: if called multiple times with the same inputs, it should invoke the same transactions and steps with the same inputs in the same order. 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 function. Instead, you should do all database operations in transactions and all other non-deterministic operations in steps. You can safely invoke these methods from a workflow.
For example, don't do this:
class Example {
@Workflow()
static async exampleWorkflow(ctxt: WorkflowContext) {
// Don't make an HTTP request in a workflow function
const body = await fetch("https://example.com").then(r => r.text());
await ctxt.invoke(Example).exampleTransaction(body);
}
}
Do this instead:
class Example {
@Step()
static async fetchBody(ctxt: StepContext) {
// Instead, make HTTP requests in steps
return await fetch("https://example.com").then(r => r.text());
}
@Workflow()
static async exampleWorkflow(ctxt: WorkflowContext) {
const body = await ctxt.invoke(Example).fetchBody();
await ctxt.invoke(Example).exampleTransaction(body);
}
}
Workflow Identity
Every time you execute a workflow, that execution is assigned a unique identity, represented as a UUID.
You can access this UUID through the context.workflowUUID
field.
Workflow identities are important for communicating with workflows and developing interactive workflows.
For more information on workflow communication, see our guide.
Asynchronous Workflows
Because workflows are often long-running, DBOS supports starting workflows asynchronously without waiting for them to complete.
When you start a workflow from a handler or another workflow with handlerCtxt.startWorkflow
or workflowCtxt.startWorkflow
, the invocation returns a workflow handle:
@GetApi(...)
static async exampleHandler(handlerCtxt: HandlerContext, ...) {
const handle = await handlerCtxt.startWorkflow(Class).workflow(...);
}
Calls to start a workflow resolve as soon as the handle is safely created; at this point the workflow is guaranteed to run to completion. This behavior is useful if you need to quickly acknowledge receipt of an event then process it asynchronously (for example, in a webhook).
You can also retrieve another workflow's handle using its identity:
@GetApi(...)
static async exampleHandler(ctxt: HandlerContext, workflowIdentity: string, ...) {
const handle = await ctxt.retrieveWorkflow(workflowIdentity);
}
To wait for a workflow to complete and retrieve its result, await handle.getResult()
:
const handle = await ctxt.retrieveWorkflow(workflowIdentity)
const result = await handle.getResult();
For more information on workflow handles, see their reference page.
Workflow Queues
By default, the startWorkflow
methods described above always start the target workflow immediately. If it is desired to control the concurrency or rate of workflow invocations, a queue
can be provided to startWorkflow
.
In the example below, the workflows started on example_queue
will have the following restrictions:
- No more than 10 will be executing concurrently across all DBOS instances
- No more than 50 will be started in any 30-second period
const example_queue = new WorkflowQueue("example_queue", 10, {limitPerPeriod: 50, periodSec: 30});
// ...
@GetApi(...)
static async exampleHandler(handlerCtxt: HandlerContext, ...) {
const handle = await handlerCtxt.startWorkflow(Class, undefined, example_queue).workflow(...);
}
Workflow Management
You can use the DBOS Transact CLI to manage your application's workflows. It provides the following commands:
npx dbos workflow list
: List workflows run by your application. Takes in parameters to filter on time, status, user, etc.npx dbos workflow get <uuid>
: Retrieve the status of a workflow.npx dbos workflow cancel <uuid>
: Cancel a workflow so it is no longer automatically retried or restarted. Active executions are not halted and the workflow can still be manually retried or restarted.npx dbos workflow resume <uuid>
: Resume a workflow from the last step it executed, keeping its identity UUID.npx dbos workflow restart <uuid>
: Resubmit a workflow, restarting it from the beginning with the same arguments but a new identity UUID.
Further Reading
To learn how to make workflows (or other functions) idempotent, see our idempotency guide.
To learn how to make workflows interactive (for example, to handle user input), see our workflow communication guide.