• Docs
  • Changelog
Sign in
Get started
    • Home
    • Your first deploy
    • Free instances
    • Platform features by plan
    • Use with coding agents
    • FAQ
    • Templates
      • Migrate from…
        • Heroku
        • Railway
    • Compute plans
    • SandboxesBeta
      • Service types
        • Which to use?
        • Static sites
        • Web services
        • Private services
        • Background workers
        • Cron jobs
        • Workflows
        • Multi-service architectures
      • Data storage
        • Render Postgres
        • Render Key Value (Redis®-compatible)
        • Persistent disks
    • How deploys work
    • Supported languages
    • Git providers (GitHub, etc.)
    • Build pipeline
    • Deploy hooks
    • Monorepo support
    • Troubleshooting deploys
      • Previewing changes
        • Single-service previews
        • Preview environments (multi-service)
      • Docker deploys
        • Overview
        • Deploy a registered image
        • Using secrets
      • Runtime
        • Native runtimes
        • Environment variables & secrets
        • Default environment variables
    • Intro to sandboxes
    • Limits
      • How to…
        • Create & configure sandboxes
        • Execute commands
        • Manage files
        • Manage snapshots
      • Use with…
        • Claude Managed Agents
        • Cursor Cloud Agents
        • OpenAI Agents API
      • SDK/CLI reference
        • TypeScript SDK
        • Python SDK
        • CLI commands
    • Intro to workflows
    • Tutorial
    • Limits & pricing
      • How to…
        • Define tasks
        • Define flowsNew
        • Trigger runs
        • Develop locally
      • SDK reference
        • TypeScript SDK
        • Python SDK
      • Example workflows
        • TypeScript examples
        • Python examples
    • Overview
    • Create & connect
    • Database credentials
    • Extensions
    • Version upgrades
    • Logical replication
    • Legacy instance types
      • Database recovery
        • Recovery & backups
        • High availability
      • Database performance
        • Read replicas
        • Connection pooling
        • Admin apps
        • Repacking tables
        • Troubleshooting performance
    • Regions
    • Inbound IP rules
    • Edge caching
    • WebSockets
    • TLS certificates
      • Custom domains
        • Overview
        • Cloudflare
        • Namecheap
        • Other DNS providers
      • Internal traffic
        • Private network
        • Private links
      • Outbound traffic
        • Default IP ranges
        • Dedicated IP addresses
        • Outbound bandwidth
        • Handling network changes
      • Static site config
        • Custom response headers
        • Redirects & rewrites
    • The Render Dashboard
    • SSH / Shell access
    • Projects & environments
      • Service actions
        • Scaling
        • Rollbacks
        • Maintenance mode
        • One-off jobs
      • Infrastructure as code
        • Blueprints overview
        • render.yaml reference
        • Terraform provider
    • In-dashboard metrics
    • Health checks
    • Uptime best practices
      • Logging
        • In-dashboard logs
        • Log streams (syslog/HTTPS)
      • OpenTelemetry (OTel) metrics
        • Provider setup
        • Metrics reference
      • Notifications
        • Webhooks
        • Email / Slack
    • MCP Server
      • Render CLI
        • Overview
        • Command reference
      • REST API
        • Overview
        • API reference
      • Third-party connections
        • OIDC support
        • Datadog
    • Workspaces, members, & roles
    • Login settings
    • Audit logs
      • Organizations
        • Overview
        • SAML SSO
    • DDoS protection
    • Platform maintenance
      • Compliance
        • Overview
        • HIPAA (US health data)
        • HIPAA best practices
        • Shared responsibility model
        • Pentest policy
    • Heroku vs Render
    • Vercel vs Render

Defining Flows

Create sequences of checkpointed steps.

After you create your first workflow, you can start defining flows 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.

From your Python project directory:

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

After installing, make sure to add render>=1.7.0 as a dependency in your application's requirements.txt, pyproject.toml, or equivalent.

Basic example

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

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.

This includes everything required to define a workflow:

  1. We import the Workflows class and FlowContext type from the Render SDK, then initialize a Workflows object as app.
  2. We apply the @app.flow decorator to register a function as a flow (in this case, format_message).
    • The decorator accepts a number of optional parameters (see Configuring flow behavior).
    • The flow 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.
  4. We call app.start() in our code's entry point.
    • On Render, this is what kicks off the flow registration process and the execution of each run.

In Python, flow functions and step functions must be synchronous (def, not async def). Each call to ctx.step() returns when the step completes.

Organizing definitions

You can define your workflow's flows 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.

Initialize a Workflows object in each file where you define flows, then incorporate those objects into the one in your workflow's entry point:

To register all of your flows and tasks, your workflow's entry point (commonly main.py) imports and incorporates the Workflows apps from each other file using the Workflows.from_workflows() method. Pass additional apps as additional positional arguments.

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, flow runs execute on the Flex compute plan (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 steps 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 flow run 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:

Provide your flow's timeout to the @app.flow decorator via the timeout_seconds parameter:

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().

Each call to ctx.step() provides a step name, a function, and its positional arguments. The name is required for every step, including lambdas. Keyword arguments are reserved for step options; pass the function's inputs positionally.

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 step 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:

Use ThreadPoolExecutor to overlap synchronous I/O inside the step function. In this example, fetch_record is a plain synchronous 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:

  • Out-of-memory (OOM) errors
  • Timeouts
  • Uncaught errors in the flow function
  • Determinism errors
  • Errors from exceeding a flow limit

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

Defining Flows
  • First: Install the Render SDK
  • Basic example
  • Organizing definitions
    • Naming flows
  • Function arguments
    • Argument format requirements
  • Configuring flow behavior
    • Compute plan
    • Timeout
  • Configuring step behavior
    • Retry settings
    • Parallelizing work within a step
  • Determinism and side effects
  • Flow replay
All Systems Operational
© Render Inc. All rights reserved.