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.
# ... existing imports + app = Workflows() ...
@app.taskdef greet(name: str) -> str: return f"Hello, {name}!"
# ... existing calculate_square, sum_squares, flip_coin, __main__ block ...// ... 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:
$render workflows tasks list --local? Select a task: (Use arrow keys)calculateSquaresumSquaresflipCoin> greet
- Select `greet` Use the arrow keys, hit Enter.
- Choose `run` From the action menu.
- 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"}. - Watch the result The CLI prints live logs and the return value:
"Hello, world!".
You can. The SDK client (Render/RenderAsync in Python, Render in TypeScript) talks to the local server when RENDER_USE_LOCAL_DEV=true is set. You’ll wire it up against a deployed task in Step 4 - same code, different env var.
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.
@app.taskasync def sum_squares(a: int, b: int) -> int: result1, result2 = await asyncio.gather( calculate_square(a), calculate_square(b), ) return result1 + result2const 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:
@app.taskasync def fan_out(n: int) -> list[int]: return list(await asyncio.gather(*[calculate_square(i) for i in range(n)]))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.
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