Workflows and Steps

Workflows (a.k.a. durable functions) are a programming model for building long-running, stateful application logic that can maintain their execution state across restarts, failures, or user events. Unlike traditional serverless functions that lose all state when they terminate, workflows persist their progress and can resume exactly where they left off.

Moreover, workflows let you easily model complex multi-step processes in simple, elegant code. To do this, we introduce two fundamental entities:

  1. Workflow Functions: Functions that orchestrate/organize steps
  2. Step Functions: Functions that carry out the actual work

Workflow Functions

Directive: "use workflow"

Workflow functions define the entrypoint of a workflow and organize how step functions are called. This type of function does not have access to the Node.js runtime, and usable npm packages are limited.

Although this may seem limiting initially, this feature is important in order to suspend and accurately resume execution of workflows.

It helps to think of the workflow function less like a full JavaScript runtime, and more like "stitching together" various steps using conditionals, loops, try/catch handlers, Promise.all, and other language primitives.

export async function processOrderWorkflow(orderId: string) {
  "use workflow"; 

  // Orchestrate multiple steps
  const order = await fetchOrder(orderId);
  const payment = await chargePayment(order);

  return { orderId, status: 'completed' };
}

Key Characteristics:

  • Runs in a sandboxed environment without full Node.js access
  • All step results are persisted to the event log
  • Must be deterministic to allow resuming after failures

Determinism in the workflow is required in order to resume to workflow from a suspension. Essentially, the workflow code gets re-run multiple times during its lifecycle, each time, using an event log to resume the workflow to the correct spot.

The sandboxed environment that workflows are run in already ensures determinism. For instance, Math.random and Date constructors are fixed in workflow runs, so you are safe to use them and the framework ensures that the values don't change across replays.

Step Functions

Directive: "use step"

Step functions perform the actual work in a workflow and have full runtime access.

async function chargePayment(order: Order) {
  "use step"; 

  // Full Node.js access - use any npm package
  const stripe = new Stripe(process.env.STRIPE_KEY);

  const charge = await stripe.charges.create({
    amount: order.total,
    currency: 'usd',
    source: order.paymentToken
  });

  return { chargeId: charge.id };
}

Key Characteristics:

  • Full Node.js runtime and npm package access
  • Automatic retry on errors
  • Results persisted for replay

By default, steps have a maximum of 3 retry attempts before they fail and propagate the error over to the workflow. Learn more about errors and retrying in the Errors & Retrying page.

Step functions are primarily meant to be used inside a workflow.

Calling a step from outside a workflow, or from another step will essentially run the step in the same process like a normal function (in other words, the use step directive is a no-op).


Suspension and Resumption

Workflow functions have the ability to automatically suspend while they wait on asynchronous work. While suspended, the workflow's state is stored via the event log and no compute resources are used until the workflow resumes execution.

There are multiple ways a workflow can suspend:

  • Waiting on a step function: the workflow yields while the step runs in the step runtime.
  • Using sleep() to pause for some fixed duration.
  • Awaiting on a promise returned by createWebhook(), which resumes the workflow when an external system passes data into the workflow.
import { sleep, createWebhook } from 'workflow';

export async function documentReviewProcess(userId: string) {
  "use workflow";

  await sleep("1 month"); // Sleep will suspend without consuming any resources

  // Create a webhook for external workflow resumption
  const webhook = createWebhook();

  // Send the webhook url to some external service or in an email, etc.
  await sendHumanApprovalEmail("Click this link to accept the review", webhook.url)

  const data = await webhook; // The workflow suspends till the URL is resumed

  console.log("Document reviewed!")
}

Writing Workflows

Basic Structure

The simplest workflow consists of a workflow function and one or more step functions.

// Workflow function (orchestrates the steps)
export async function greetingWorkflow(name: string) {
  "use workflow";

  const message = await greet(name);
  return { message };
}

// Step function (does the actual work)
async function greet(name: string) {
  "use step";

  // Access Node.js APIs
  const message = `Hello ${name} at ${new Date().toISOString()}`;
  console.log(message);
  return message;
}

Project structure

While you can organize workflow and step functions however you like, we find that larger projects benefit from some structure:

index.ts
steps.ts
index.ts
transcribeUpload.ts
generateVideo.ts
notifyUser.ts
validateInput.ts
logActivity.ts

You can choose to organize your steps into a single steps.ts file or separate files within a steps folder. The shared folder is a good place to put common steps that are used by multiple workflows.

Splitting up steps and workflows will also help avoid most bundler related bugs with the Workflow DevKit.