# Defining Workflow Tasks — Specify units of work to run on Render.
> *Render Workflows is in public beta.*
>
> During the beta, bugs or changes in API/SDK behavior are possible as we continue refining the product. We welcome any and all feedback at *workflows-feedback@render.com*.
After you [create your first workflow](/workflows-tutorial), 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.
**TypeScript**
From your TypeScript project directory:
```shell
npm install @renderinc/sdk
```
(Or `pnpm install`, `bun add`, etc.)
*If you already have the SDK installed,* make sure you're using version `^0.5.0` or later:
```shell
npm install @renderinc/sdk@latest
```
After installing, make sure `@renderinc/sdk` is listed as a dependency in your `package.json` file at version `^0.5.0` or later.
**Python**
From your Python project directory:
```shell
pip install render_sdk
```
*If you already have the SDK installed,* make sure you're using version `0.6.0` or later:
```shell
pip install --upgrade render_sdk
```
After installing, make sure to add `render_sdk>=0.6.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 task:
**TypeScript**
```typescript:index.ts
import { task } from '@renderinc/sdk/workflows'
const calculateSquare = task(
{ name: 'calculateSquare' },
function calculateSquare(a: number): number {
return a * a
}
)
```
This includes everything required to define a workflow:
1. We import the `task` function from the Render SDK.
2. We call `task(...)` to register a function (`calculateSquare`) as a task.
- You can provide optional arguments to configure timeout, retry logic, and more. For details, see [Task-level config.](#task-level-config)
**Python**
```python:main.py
from render_sdk import Workflows
app = Workflows()
@app.task
def calculate_square(a: int) -> int:
return a * a
if __name__ == "__main__":
app.start()
```
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.](#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 tasks across multiple files in your project repo:
**TypeScript**
```typescript:math-tasks.ts
import { task } from '@renderinc/sdk/workflows'
export const add = task(
{ name: 'add' },
function add(a: number, b: number): number {
return a + b
}
)
```
```typescript:text-tasks.ts
import { task } from '@renderinc/sdk/workflows'
export const capitalize = task(
{ name: 'capitalize' },
function capitalize(s: string): string {
return s.toUpperCase()
}
)
```
```typescript:index.ts
import './math-tasks'
import './text-tasks'
```
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.
**Python**
```python:math_tasks.py
from render_sdk import Workflows
app = Workflows()
@app.task
def add(a: int, b: int) -> int:
return a + b
```
```python:text_tasks.py
from render_sdk import Workflows
app = Workflows()
@app.task
def capitalize(s: str) -> str:
return s.upper()
```
```python:main.py
from render_sdk import Workflows
from math_tasks import app as math_app
from text_tasks import app as text_app
app = Workflows.from_workflows(math_app, text_app) # highlight-line
if __name__ == "__main__":
app.start() # SDK entry point
```
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()`](/workflows-sdk-python#workflowsfrom-workflows) method.
## Task arguments
*A task function can define any number of arguments.* This example task takes three arguments of different types:
**TypeScript**
```typescript
const myTask = task(
{ name: 'myTask' },
function myTask(a: number, b: string, c: boolean): number {
// ...
}
)
```
**Python**
```python
@app.task
def my_task(arg1: int, arg2: str, arg3: bool) -> int:
# ...
```
*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.
Both the TypeScript and Python SDKs support setting default argument values:
**TypeScript**
```typescript
const myTask = task(
{ name: 'myTask' },
function myTask(arg1: number = 3): number {
// ...
}
)
```
**Python**
```python
@app.task
def my_task(arg1: int = 3) -> int:
# ...
```
## Task-level config
### Instance type (compute specs)
By default, task runs execute on Render's *Standard* instance type (1 CPU, 2GB RAM). You can override this on a per-task basis.
Set a task's instance type with the following syntax:
**TypeScript**
```typescript
const myTask = task(
{
name: 'myTask',
plan: 'starter' // highlight-line
},
function myTask(a: number): number {
return a * a
}
)
```
**Python**
```python
@app.task(
plan="starter" # highlight-line
)
def my_task(a: int) -> int:
return a * a
```
The following instance types are supported for all workspaces:
| Instance Type | Specs |
| --- | --- |
| *`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 Type | Specs |
| --- | --- |
| *`pro_plus`* | 4 CPU 8 GB RAM |
| *`pro_max`* | 8 CPU 16 GB RAM |
| *`pro_ultra`* | 16 CPU 32 GB RAM |
See [pricing details.](/workflows-limits#instance-types-compute-specs)
### Timeout
By default, task runs time out after 2 hours. You can override this on a per-task basis to any value between *30 seconds* and *24 hours*.
**TypeScript**
Provide your task's timeout to `task(...)` via the `timeoutSeconds` option:
```typescript
const myTask = task(
{
name: 'myTask',
timeoutSeconds: 86400 // 24 hours in seconds
},
function myTask(a: number): number {
return a * a
}
)
```
**Python**
Provide your task's timeout to the `@app.task` decorator via the `timeout_seconds` argument:
```python
@app.task(
timeout_seconds=86400 # 24 hours in seconds
)
def my_task(a: int) -> int:
return a * a
```
### Retry logic
Task runs can automatically *retry* if they fail. A run is considered to have failed if its function raises an exception or throws an error 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 with the following syntax:
**TypeScript**
```typescript{6-10}
import { task } from '@renderinc/sdk/workflows'
const flipCoin = task(
{
name: 'flipCoin',
retry: {
maxRetries: 3, // Retry up to 3 times (i.e., 4 total attempts)
waitDurationMs: 1000, // Set a base retry delay of 1 second
backoffScaling: 1.5 // Increase delay by 50% after each retry (1s, 1.5s, 2.25s)
}
},
function flipCoin(): string {
if (Math.random() < 0.5) {
throw new Error('Flipped tails! Retrying.')
}
return 'Flipped heads!'
}
)
```
**Python**
```python{1,7-11}
from render_sdk import Workflows, Retry
import random
app = Workflows()
@app.task(
retry=Retry(
max_retries=3, # Retry up to 3 times (i.e., 4 total attempts)
wait_duration_ms=1000, # Set a base retry delay of 1 second
backoff_scaling=1.5 # Increase delay by 50% after each retry (1s, 1.5s, 2.25s)
)
)
def flip_coin() -> str:
if random.random() < 0.5:
raise Exception("Flipped tails! Retrying.")
return "Flipped heads!"
```
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. Like other runs, these *chained runs* each execute in their own instance.
> *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
**TypeScript**
The simple `sumSquares` task below chains two parallel runs of the `calculateSquare` task:
```typescript:math-tasks.ts
import { task } from '@renderinc/sdk/workflows'
const calculateSquare = task(
{ name: 'calculateSquare' },
function calculateSquare(a: number): number {
return a * a
}
)
// A task that chains two parallel runs
const sumSquares = task(
{ name: 'sumSquares' },
async function sumSquares(a: number, b: number): Promise {
const [result1, result2] = await Promise.all([
calculateSquare(a),
calculateSquare(b)
])
return result1 + result2
}
)
```
*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., `calculateSquare` above).
- *However,* this call doesn't return the function's defined return value!
- *Instead,* this triggers a new run and returns a Promise.
- As shown, you can `await` this result to obtain the run's _actual_ return value.
**Python**
The simple `sum_squares` task below chains two parallel runs of the `calculate_square` task:
```python:math_tasks.py
from render_sdk import Workflows
import asyncio
app = Workflows()
# A task that chains two parallel runs
@app.task
async def sum_squares(a: int, b: int) -> int: # Must be async to await chained runs
result1, result2 = await asyncio.gather(
calculate_square(a),
calculate_square(b)
)
return result1 + result2
@app.task
def calculate_square(a: int) -> int:
return a * a
```
*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](/workflows-running). Note that this is not tracked as a chaining relationship when visualizing task execution in the [Render Dashboard](https://dashboard.render.com).
### 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.
**TypeScript**
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:
```typescript
import { task } from '@renderinc/sdk/workflows'
const processImage = task(
{ name: 'processImage' },
function processImage(imageUrl: string): {
url: string
thumbnailUrl: string
success: boolean
} {
// Image processing logic goes here
return {
url: imageUrl,
thumbnailUrl: `${imageUrl}_thumb.jpg`,
success: true
}
}
)
const processPhotoUpload = task(
{ name: 'processPhotoUpload' },
async function processPhotoUpload(imageUrls: string[]): Promise<{
total: number
processed: number
failed: number
results: Array<{ url: string; thumbnailUrl: string; success: boolean }>
}> {
// Process all images in parallel by chaining a run for each
const results = await Promise.all( // highlight-line
imageUrls.map((url) => processImage(url)) // highlight-line
) // highlight-line
const numSuccessful = results.filter((r) => r.success).length
const numFailed = results.length - numSuccessful
return {
total: imageUrls.length,
processed: numSuccessful,
failed: numFailed,
results
}
}
)
```
*If you don't use `Promise.all` or a similar function, chained runs execute serially.*
For example:
```typescript{4-6}
const sumSquaresSlower = task(
{ name: 'sumSquaresSlower' },
async function sumSquaresSlower(a: number, b: number): Promise {
// ⚠️ Not parallel!
const result1 = await calculateSquare(a)
const result2 = await calculateSquare(b) // Executes after first run completes
return result1 + result2
}
)
const calculateSquare = task(
{ name: 'calculateSquare' },
function calculateSquare(a: number): number {
return a * a
}
)
```
**Python**
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:
```python
from render_sdk import Workflows
import asyncio
app = Workflows()
@app.task
async def process_photo_upload(image_urls: list[str]) -> dict:
# Process all images in parallel by chaining a run for each
results = await asyncio.gather( # highlight-line
*[process_image(url) for url in image_urls] # highlight-line
) # highlight-line
num_successful = sum(1 for r in results if r["success"])
num_failed = len(results) - num_successful
return {
"total": len(image_urls),
"processed": num_successful,
"failed": num_failed,
"results": results
}
@app.task
def process_image(image_url: str) -> dict:
# Image processing logic goes here
return {
"url": image_url,
"thumbnail_url": f"{image_url}_thumb.jpg",
"success": True
}
```
*If you don't use `asyncio.gather` or a similar function, chained runs execute serially.*
For example:
```python{3-5}
@app.task
async def sum_squares_slower(a: int, b: int) -> int:
# ⚠️ Not parallel!
result1 = await calculate_square(a)
result2 = await calculate_square(b) # Executes after first run completes
return result1 + result2
@app.task
def calculate_square(a: int) -> int:
return a * a
```
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.*
---
##### Appendix: Glossary definitions
###### task
A function you can execute on its own compute as part of a *workflow*.
Each execution of a task is called a *run*.
Related article: https://render.com/docs/workflows-defining.md
###### run
A single execution of a workflow *task*.
A run spins up in its own *instance*, executes, returns a value, and is deprovisioned.
Related article: https://render.com/docs/workflows-running.md
###### instance type
Specifies the RAM and CPU available to your service's *instances*.
Common instance types for a new web service include:
- *Free*: 512 MB RAM / 0.1 CPU
- *Starter*: 512 MB RAM / 0.5 CPU
- *Standard*: 2 GB RAM / 1 CPU
For the full list, see the [pricing page](/pricing#services).
###### run chaining
Triggering a new *task run* by calling its function from an in-progress run.
All runs in a chain belong to the same *workflow*.
Related article: https://render.com/docs/workflows-defining.md
###### instance
A virtual machine that runs your service's code on Render.
You can select from a range of *instance types* with different compute specs.