Defining Workflow Tasks

Specify units of work to run on Render.

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

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.0 or later:

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

From your Python project directory:

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

After installing, make sure to add render>=1.0.1 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 :

This includes everything required to define a workflow:

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

This includes everything required to define a workflow:

  1. We import the Workflows class and TaskContext type from the Render SDK, then initialize a Workflows object as app.
  2. We apply the @app.task decorator to register a function as a task (in this case, calculate_square).
    • The decorator accepts a number of optional arguments (see Configuring task behavior).
    • The task function must take a TaskContext object as its first argument (Render provides this object to each task run automatically).
  3. We call app.start() in our code's entry point.
    • On Render, this is what kicks off the task registration process and the execution of each run.

Organizing tasks

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

In this example, task definitions are distributed across two files: math-tasks.ts and text-tasks.ts. By setting your workflow's start command to run the JS output of index.ts, you ensure that all tasks are imported and registered.

In this example, task definitions are distributed across two files: math_tasks.py and text_tasks.py.

To register all of your 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.

Task arguments

Every task function must take a TaskContext object as its first argument:

Render passes this object to each task run automatically.

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

You provide values for these arguments whenever you trigger or chain a task run.

You can set default values for arguments:

Argument format requirements

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

Configuring task behavior

Instance type (compute specs)

By default, execute on the Standard (1 CPU, 2 GB RAM).

You can specify a different instance type to use for a given task with the following syntax:

The following instance types are available for workflow tasks:

  • flex: up to 1 CPU / up to 4 GB RAM
  • starter: 0.5 CPU / 512 MB RAM
  • standard: 1 CPU / 2 GB RAM
  • 2c-4g: 2 CPU / 4 GB RAM (formerly pro)
  • 2c-8g: 2 CPU / 8 GB RAM
  • 4c-8g: 4 CPU / 8 GB RAM (formerly pro_plus)
  • 4c-16g: 4 CPU / 16 GB RAM (formerly pro_max)

See pricing details.

Timeout

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

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

Provide your task's timeout to the @app.task decorator via the timeout_seconds argument:

Retry logic

can automatically retry if they fail. Render considers a run to have failed in any case where its function does not complete normally. This includes:

  • The task function raises an exception or throws an error.
  • The run times out.
  • The run exceeds its instance's compute limits.
  • The run's underlying instance encounters an unexpected error.

Default retry behavior

If you don't customize a task's retry behavior, it uses the following defaults:

  • Retry a failed run up to three times (i.e., four total attempts).
  • Wait one second before attempting the first retry.
  • Double the wait time after each retry (i.e., one second, two seconds, four seconds).

Customizing retries

You can customize retry behavior on a per-task basis. Every run of a task uses the same retry settings.

Provide retry settings with the following syntax:

This contrived example defines a task that "flips a coin" and raises an exception/error when it "flips tails", causing the run to fail and retry according to its settings.

Chaining task runs

A can trigger additional task runs as part of its execution. These each execute in their own and return their result to their parent run:

run_agent()30s
gather_context()
execute_skills()
compose_response()
fetch_docs()
search_web()

All tasks in a run chain must belong to the same workflow service.

Chaining runs is an essential part of Render Workflows. It enables you to quickly fan out independent units of work across distributed compute, then roll up the entirety of that work into a unified result.

When to chain runs

Chaining runs is most useful when different parts of a larger job benefit from their own compute resources and/or retry boundaries.

For simpler, resource-light jobs, it can be more efficient to define the entirety of your logic in a single task.

How to chain runs

The TaskContext object passed to each task function provides a run method that you use to chain additional runs.

Let's look at an example:

The simple sumSquares task below chains two parallel runs of the calculateSquare task:

  • Chain a run by passing the corresponding task definition to ctx.run() (such as calculateSquare above).
    • You provide the run's arguments as additional parameters after the task definition.
    • You can await the Promise returned by ctx.run() to obtain the chained run's return value.
  • Any task function that chains runs should be defined as async.
    • Otherwise, it can't await the results of its chained runs.

The simple sum_squares task below chains two parallel runs of the calculate_square task:

  • Chain a run by passing the corresponding task definition to ctx.run() (such as calculate_square above).
    • You provide the run's arguments as additional parameters after the task definition.
    • You can await the result returned by ctx.run() to obtain the chained run's return value.
  • Any task function that chains runs should be defined as async.
    • Otherwise, it can't await the results of its chained runs.

Want to trigger a run of a task from a different workflow?

This requires instead using the Render SDK or Render API, as described in Running Workflow Tasks. Note that this is not tracked as a chaining relationship when visualizing task execution in the Render Dashboard.

Parallel runs

When , you'll often want to chain multiple at once to distribute independent work. Common examples include processing batches of images or analyzing different sections of a large data set.

To chain parallel runs in TypeScript, use Promise.all, Promise.allSettled, or a similar concurrency utility.

In this example, the processPhotoUpload task a separate processImage run for each element in its imageUrls argument:

If you don't use Promise.all or a similar function, chained runs execute serially.

For example:

To chain parallel runs in Python, use asyncio.gather, asyncio.TaskGroup, or a similar concurrency utility.

In this example, the process_photo_upload task a separate process_image run for each element in its image_urls argument:

If you don't use asyncio.gather or a similar function, chained runs execute serially.

For example:

Serial execution is helpful when one depends on the result of another. However, it can significantly slow execution for runs that are completely independent. Parallelize wherever your use case allows.