Defining Workflow Tasks

Specify units of work to run on Render.

Render Workflows is in limited early access.

During the early access period, the Workflows API and SDKs might introduce breaking changes.

Request Early Access

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 only for Python.

TypeScript support is nearing release (see GitHub).

SDKs for additional languages are planned for future releases.

The Render SDK is required to define and register workflow tasks.

From your Python project directory:

Make sure to add render_sdk as a dependency to 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 Workflows class from the Render SDK and initialize it as app.
  2. We apply the @app.task decorator to a function (calculate_square) to mark it as a task.
    • This decorator accepts a number of optional arguments. For details, see Task-level config.
  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.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

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

Argument and return types must be JSON-serializable. Your applications provide task arguments in a JSON array or object via the Render API. A task's result is also returned as JSON.

Task arguments are required by default. However, the Render SDK for Python does support setting default argument values:

If you attempt to trigger a run with missing arguments (or too many arguments), the run will fail. Argument values can be null, as long as your function's logic supports this.

Task-level config

Instance type (compute specs)

By default, execute on Render's Standard (1 CPU, 2GB RAM). You can override this on a per-task basis.

Set a task's instance type with the following syntax:

The following instance types are supported for all workspaces:

Instance TypeSpecs

starter

0.5 CPU
512 MB RAM

standard (default)

1 CPU
2 GB RAM

pro

2 CPU
4 GB RAM

If you have more resource-intensive workloads, you can request access to the following larger instance types:

Instance TypeSpecs

pro_plus

4 CPU
8 GB RAM

pro_max

8 CPU
16 GB RAM

pro_ultra

16 CPU
32 GB RAM

See pricing details.

Timeout

By default, time out after 2 hours. You can override this on a per-task basis to any value between 30 seconds and 24 hours.

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

Retry logic

can automatically retry if they fail. A run is considered to have failed if its function raises an exception instead of returning a value.

Retries are useful for tasks that might be affected by transient failures, such as network errors or timeouts.

Default retry behavior

By default, task runs use the following retry logic:

  • Retry up to 3 times (i.e., 4 total attempts)
  • Wait 1 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 to the @app.task decorator with the following syntax:

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

Chaining task runs

A can trigger additional task runs. Like other runs, these each execute in their own .

Workflows overview

When should I chain runs?

Chaining is most helpful when different parts of a larger job benefit from long-running, individually provisioned compute.

For simple jobs (such as the very basic example below), it's more efficient to define the entirety of your logic in a single task.

Example

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

When chaining runs:

  • In most cases, your chaining task's function should be defined as async.
    • Otherwise, it can't await the results of its chained runs.
  • You chain a run by calling the corresponding task function (e.g., calculate_square above).
    • However, this call doesn't return the function's defined return value!
    • Instead, this triggers a new run and returns a special TaskInstance object.
    • As shown, you can await this object to obtain the run's actual return value.

Task functions can call other functions that are not marked as tasks. These functions execute and return as normal (they do not trigger chained runs).

Need to run a task defined in 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 kick off multiple in parallel, such as to chunk a large workload into smaller independent pieces. Common examples include processing batches of images or analyzing different sections of a large document.

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.