Defining Flows

Create sequences of checkpointed steps.

After you create your first workflow, you can start defining for it. This article describes supported syntax and configuration options.

First: Install the Render SDK

The Render SDK is currently available for TypeScript and Python.

SDKs for additional languages are planned for future releases.

The Render SDK is required to define and register flows.

From your TypeScript project directory:

(Or pnpm install, bun add, etc.)

If you already have the SDK installed, make sure you're using version 1.7.0 or later:

After installing, make sure @renderinc/sdk is listed as a dependency in your package.json file at version ^1.7.0 or later.

Basic example

Let's start with a "minimum viable workflow" that defines a single . This flow formats a message in two :

This includes everything required to define a workflow:

  1. We import the flow function and FlowContext type from the Render SDK.
  2. We call flow(...) to register a function as a flow (in this case, formatMessage).
    • The first parameter to flow(...) is an object containing configuration options (only name is required). For other supported options, see Configuring flow behavior.
    • The second parameter is the flow function itself. This function must take a FlowContext object as its first argument (Render provides this object to each flow run automatically).
  3. We call ctx.step() to run each step and obtain its return value.

Organizing definitions

You can define your workflow's across multiple files in your project repo:

Define and export flows from individual files, then import those files from your workflow's entry point:

By setting your workflow's start command to run the JS output of index.ts, you ensure that the imported definitions are registered. Add imports for any other files that define flows or tasks.

Naming flows

Tasks and flows share a namespace. Use a name that is unique across all tasks and flows in the workflow. In TypeScript, registering another task or flow with the same name replaces the earlier definition. In Python, duplicate names cause a registration error.

Step names do not register separate definitions. You provide them when running steps.

Function arguments

Every flow function must take a FlowContext object as its first argument:

Render passes this object to each flow run automatically.

A flow function can define any number of additional arguments. This example flow takes three additional arguments of different types:

You provide values for these non-ctx arguments whenever you trigger a flow run.

You can set default values for arguments:

Argument format requirements

  • A flow's argument types (and its return type) must be JSON-serializable.
    • Argument values are passed to a flow run's instance as JSON, and a flow run's result is returned as JSON.
  • The combined size of all arguments passed to a single flow run cannot exceed 4 MB.
    • Otherwise, the request fails with an error.

Configuring flow behavior

Compute plan

By default, runs execute on the Flex (up to 1 CPU, up to 4 GB RAM).

You can specify a different compute plan to use for a given flow with the following syntax:

The following compute plans are available for flows:

CPURAMPlan ID

Up to 1 CPU

Up to 4 GB

flex

24 GB

2c-4g

28 GB

2c-8g

48 GB

4c-8g

416 GB

4c-16g

832 GB

8c-32g

Providing starter or standard is equivalent to providing flex.

Because execute on the same instance as their flow, you do not specify per-step compute plans.

See pricing details.

Timeout

By default, each execution attempt of a times out after 2 hours if its function hasn't returned yet. You can override this on a per-flow basis to any value between 30 seconds and 24 hours.

Provide your flow's timeout to flow(...) via the timeoutSeconds option:

Each flow recovery starts a new execution attempt with a fresh timeout. A run's total duration can therefore exceed its configured timeout.

In Python, you can also set default_plan and default_timeout on the Workflows object. Individual flows can override these settings.

Configuring step behavior

Within a flow definition, use the FlowContext object's step method to run a plain function as a step. Steps run inside the flow's process, on the same compute as the flow. They are not registered as separate definitions, but each step and its attempts appear in the flow run's execution viewer.

The basic example runs two steps to format a message:

Each call to ctx.step() provides a step name, a function, and an array of arguments to pass to that function.

For all supported options, see FlowContext.step().

The step's function receives its arguments without a context argument. Pass a plain function, not a registered task or flow definition.

Step arguments and return values must be JSON-serializable. For example, TypeScript-defined steps can't return Date objects, class instances, NaN, or Infinity.

Step results pass through JSON serialization, even during the initial execution.

A step function cannot make calls on the flow's context.

Flows cannot run registered tasks. To share code between a flow and a task, define a plain function to call as a step and call the same function from a registered task.

For step limits, see Limits and pricing.

Retry settings

A can automatically retry if its initial attempt fails, such as when the step function raises an exception or throws an error.

Each retry reruns the step's function on the flow's instance. If a step reaches its retry limit with no successful attempts, the step is marked as failed. An unhandled step failure fails the flow run.

If a step's return value fails JSON serialization, the step fails without retrying.

Default retry behavior

If you don't customize a step's retry behavior, it does not retry (one total attempt).

Customizing retries

You can customize retry behavior on a per-step basis.

Provide retry settings to ctx.step() with the following syntax:

This contrived example defines a step that "flips a coin" and raises an exception/error when it "flips tails", causing the attempt to fail. The step retries according to its settings, up to three times.

Parallelizing work within a step

Each ctx.step() call must finish before the next starts:

  • In TypeScript, await each ctx.step() call before making the next. Concurrent calls, including calls in Promise.all, throw an error.
  • In Python, each ctx.step() call blocks until the step finishes.

You can still perform concurrent work inside a single step, such as fetching multiple records from an API:

Use Promise.all or a similar concurrency utility inside the step function. In this example, fetchRecord is a plain asynchronous function defined elsewhere that fetches one record:

Each batch above is a single step. Render saves the batch's combined result, not the result of each individual request. If the step retries or re-executes after an interruption, the entire batch can run again.

All work within a step shares the flow's compute plan. A plan with more CPU provides more processing capacity for CPU-intensive work across multiple workers or processes. More RAM supports work with larger memory requirements. Your code controls how many workers or threads to create; changing the compute plan does not automatically parallelize your code.

Overlapping I/O with Promise.all or ThreadPoolExecutor does not require multiple CPUs. To use multiple CPUs for CPU-intensive work, consider Node.js worker threads, Python process pools, or libraries that use multiple CPUs.

Determinism and side effects

An interrupted step whose outcome was not recorded can execute again, so side effects should be safe to repeat.

Flow code must be deterministic: it must make the same ctx calls in the same order with the same inputs on replay. Put I/O, random values, and reads of the current time inside steps.

If a step's name, order, or inputs differ from the recorded history, the SDK raises a DeterminismError. This fails the flow run even if you catch the error.

Flow replay

You do not set a retry policy for a flow. If an infrastructure issue (such as instance failure) interrupts a flow run, Render automatically replays the run on a new instance. This executes the flow function from the beginning, using the recorded return values of any successfully completed steps instead of executing those steps again.

Each replay creates a new execution attempt of the original flow run. The initial attempt has "attempt number" 0, the first replay attempt has number 1, and so on. These are separate from a step's retry attempts.

All attempts of a flow run use the workflow version the run started on, even if you deploy a new version.

The following errors fail a flow run without replay:

Canceling a flow run ends it with a canceled status. Canceled runs do not replay.