# 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. 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 **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.