Defining Workflow Tasks
Specify units of work to run on Render.
After you create your first workflow, you can start defining your own tasks. 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 task:
This includes everything required to define a workflow:
- We import the
taskfunction andTaskContexttype from the Render SDK. - 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 (onlynameis required). For other supported options, see Configuring task behavior. - The second parameter is the task function itself. This function must take a
TaskContextobject as its first argument (Render provides this object to each task run automatically).
- The first parameter to
This includes everything required to define a workflow:
- We import the
Workflowsclass andTaskContexttype from the Render SDK, then initialize aWorkflowsobject asapp. - We apply the
@app.taskdecorator 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
TaskContextobject as its first argument (Render provides this object to each task run automatically).
- 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 tasks 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, task runs execute on the Standard instance type (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 RAMstarter: 0.5 CPU / 512 MB RAMstandard: 1 CPU / 2 GB RAM2c-4g: 2 CPU / 4 GB RAM (formerlypro)2c-8g: 2 CPU / 8 GB RAM4c-8g: 4 CPU / 8 GB RAM (formerlypro_plus)4c-16g: 4 CPU / 16 GB RAM (formerlypro_max)
See pricing details.
Timeout
By default, a task run 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
Task runs 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 task run can trigger additional task runs as part of its execution. These chained runs each execute in their own instance and return their result to their parent run:
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 ascalculateSquareabove).- You provide the run's arguments as additional parameters after the task definition.
- You can
awaitthe Promise returned byctx.run()to obtain the chained run's return value.
- Any task function that chains runs should be defined as
async.- Otherwise, it can't
awaitthe results of its chained runs.
- Otherwise, it can't
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 ascalculate_squareabove).- You provide the run's arguments as additional parameters after the task definition.
- You can
awaitthe result returned byctx.run()to obtain the chained run's return value.
- Any task function that chains runs should be defined as
async.- Otherwise, it can't
awaitthe results of its chained runs.
- Otherwise, it can't
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 chaining runs, 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 chains 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 chains 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 run depends on the result of another. However, it can significantly slow execution for runs that are completely independent. Parallelize wherever your use case allows.