Render Tutorials
Render Workflows quickstart

Write and run your own task

⏱ 10 min

You’ll add a greet(name) task next to the starter examples, run it through the interactive CLI, then peek at how a task can fan work out into parallel sub-runs.

1. Add a task

Open your workflows/ entry file and add the new task alongside the starter code - don’t delete what’s already there, you’ll use it in a minute.

workflows/main.py
# ... existing imports + app = Workflows() ...
@app.task
def greet(name: str) -> str:
return f"Hello, {name}!"
# ... existing calculate_square, sum_squares, flip_coin, __main__ block ...
workflows/index.ts
// ... existing import at the top ...
const greet = task(
{ name: "greet" },
function greet(name: string): string {
return `Hello, ${name}!`;
},
);
// ... existing calculateSquare, sumSquares, flipCoin definitions ...

The dev server picks code changes up automatically - no restart needed.

2. Run it through the CLI

Make sure your local task server is still running from Step 2. Then, in your second terminal:

Terminal
$render workflows tasks list --local
? Select a task: (Use arrow keys)
calculateSquare
sumSquares
flipCoin
> greet
  1. Select `greet` Use the arrow keys, hit Enter.
  2. Choose `run` From the action menu.
  3. Provide input as a JSON array Type ["world"] and hit Enter. Positional args go in a list; keyword args go in an object like {"name": "world"}.
  4. Watch the result The CLI prints live logs and the return value: "Hello, world!".

3. Where the magic happens: subtasks

Look at the sumSquares task from the starter. It calls calculateSquare twice - but those calls don’t run in the same process. Each one becomes its own task run on its own instance, and Promise.all / asyncio.gather waits for both in parallel.

workflows/main.py
@app.task
async def sum_squares(a: int, b: int) -> int:
result1, result2 = await asyncio.gather(
calculate_square(a),
calculate_square(b),
)
return result1 + result2
workflows/index.ts
const sumSquares = task(
{ name: "sumSquares" },
async function sumSquares(a: number, b: number): Promise<number> {
const [result1, result2] = await Promise.all([
calculateSquare(a),
calculateSquare(b),
]);
return result1 + result2;
},
);

That’s the same pattern that scales to a fan-out over a thousand items. You just iterate and gather:

workflows/main.py
@app.task
async def fan_out(n: int) -> list[int]:
return list(await asyncio.gather(*[calculate_square(i) for i in range(n)]))
workflows/index.ts
const fanOut = task(
{ name: "fanOut" },
async function fanOut(n: number): Promise<number[]> {
return Promise.all(
Array.from({ length: n }, (_, i) => calculateSquare(i)),
);
},
);

4. Retries, for free

The flipCoin task throws on tails. Run it a few times through the CLI - you’ll see it fail and then retry up to 3 times with a growing wait between attempts, controlled by the retry option on the task:

retry: { maxRetries: 3, waitDurationMs: 1000, backoffScaling: 1.5 }

No queue. No retry library. Just a config object.

In the `sumSquares` task, why must the function be `async` (Python) or return a `Promise` (TypeScript)?

What you learned

  • Added a `greet` task and ran it interactively with `render workflows tasks list --local`
  • Saw how subtasks become parallel runs on their own instances
  • Configured retries with a single `retry` option on the task