From 1250e2ca913be3f47490aaeff8682cdf03f143f2 Mon Sep 17 00:00:00 2001 From: Stephen Barlow Date: Mon, 6 Apr 2026 15:42:52 -0700 Subject: [PATCH 1/2] Update README and dependencies for hello world --- hello-world/README.md | 480 +++++------------------------------ hello-world/main.py | 208 ++------------- hello-world/requirements.txt | 2 +- hello-world/template.yaml | 6 +- 4 files changed, 89 insertions(+), 607 deletions(-) diff --git a/hello-world/README.md b/hello-world/README.md index afcc659..333e48b 100644 --- a/hello-world/README.md +++ b/hello-world/README.md @@ -1,477 +1,119 @@ -# Hello World - Getting Started with Render Workflows +# Hello World - Render Workflows (Python) -The simplest possible workflow example to help you understand the basics of Render Workflows. Perfect for beginners! +This hello-world example demonstrates three foundational workflow patterns: + +- A minimal task definition (`calculate_square`) +- A task that chains runs of another task (`sum_squares`) +- A task with custom retry behavior (`flip_coin`) ## What You'll Learn -This example teaches the fundamental concepts: -- **What is a Task?** - A function decorated with `@app.task` that can be executed as a workflow -- **What is a Subtask?** - A task called by another task using `await` -- **How to Orchestrate** - Combining multiple tasks to create workflows -- **How to Deploy** - Getting your first workflow running on Render +- How to define tasks with `@app.task` +- How to chain task runs using `await` and `asyncio.gather` +- How to customize retry behavior with `Retry` -## Use Case +## Example Tasks -Simple number processing to demonstrate workflow patterns without complex business logic. If you can understand this example, you can build any workflow! +### `calculate_square(a: int) -> int` -## Workflow Structure +The smallest possible task: takes one integer and returns its square. -``` -calculate_and_process (multi-step orchestrator) - ├── add_doubled_numbers - │ ├── double (subtask #1) - │ └── double (subtask #2) - └── process_numbers - ├── double (subtask for item 1) - ├── double (subtask for item 2) - └── double (subtask for item N) -``` +### `sum_squares(a: int, b: int) -> int` -## Understanding Tasks and Subtasks +Chains two runs of `calculate_square` and sums the results. -### What is a Task? - -A **task** is simply a Python function decorated with `@app.task`. It becomes a workflow step that Render can execute: +It uses `asyncio.gather(...)` to chain the two runs in parallel: ```python -from render_sdk import Workflows - -app = Workflows() - -@app.task -def double(x: int) -> int: - """A simple task that doubles a number""" - return x * 2 - -app.start() +result1, result2 = await asyncio.gather( + calculate_square(a), + calculate_square(b), +) ``` -### What is a Subtask? +### `flip_coin() -> str` -A **subtask** is when one task calls another task using `await`. This is how you compose workflows: +Simulates a coin flip: -```python -@app.task -async def add_doubled_numbers(a: int, b: int) -> dict: - # Call 'double' as a subtask using await - doubled_a = await double(a) # ← This is a subtask call! - doubled_b = await double(b) # ← This is also a subtask call! - - return { - "sum": doubled_a + doubled_b - } -``` +- Heads: Returns success +- Tails: Raises an error to trigger retry -### Why Use Subtasks? +Retry policy in this example: -1. **Reusability**: Write `double` once, use it everywhere -2. **Composition**: Build complex workflows from simple building blocks -3. **Visibility**: Render shows you each subtask execution in the dashboard -4. **Testing**: Test individual tasks independently +- max retries: `3` +- wait duration: `1000ms` +- backoff scaling: `1.5` ## Local Development ### Prerequisites + - Python 3.10+ -### Setup and Run +### Run locally ```bash -# Navigate to example directory cd hello-world - -# Install dependencies pip install -r requirements.txt - -# Run the workflow service python main.py ``` -The service will start and register all tasks. You'll see output like: - -``` -Starting Hello World Workflow Service -Registered tasks: - - double(x) - - add_doubled_numbers(a, b) - - process_numbers(numbers) - - calculate_and_process(a, b, more_numbers) -Ready to accept task executions! -``` - -## Deploying to Render - -### Service Configuration +To run tasks locally from another terminal: -**Service Type**: Workflow - -**Build Command**: ```bash -cd hello-world && pip install -r requirements.txt +render workflows tasks start calculate_square --local --input='{"a": 5}' +render workflows tasks start sum_squares --local --input='{"a": 3, "b": 4}' +render workflows tasks start flip_coin --local --input='{}' ``` -**Start Command**: -```bash -cd hello-world && python main.py -``` - -### Environment Variables - -Required: -- `RENDER_API_KEY` - Your Render API key (from Render dashboard) - -### Deployment Steps - -1. **Create Workflow Service** - - Go to Render Dashboard - - Click "New +" → "Workflow" - - Connect your repository - - Name: `hello-world-workflows` - -2. **Configure Build Settings** - - Build Command: `cd hello-world && pip install -r requirements.txt` - - Start Command: `cd hello-world && python main.py` - -3. **Set Environment Variables** - - Add `RENDER_API_KEY` in the Environment section - - Get API key from: Render Dashboard → Account Settings → API Keys - -4. **Deploy** - - Click "Create Workflow" - - Render will build and start your workflow service - -## Testing in Render Dashboard - -Once deployed, test your workflows directly in the Render Dashboard: - -### How to Test - -1. Go to your Workflow service in Render Dashboard -2. Click the **"Manual Run"** or **"Start Task"** button -3. Select the task you want to test -4. Enter the task input as JSON in the text area -5. Click **"Start task"** - -### Example Task Inputs - -**Important:** The hello-world workflow expects direct values and arrays, not JSON objects. Use `5` instead of `{"x": 5}`, and `[3, 4]` instead of `{"a": 3, "b": 4}`. - -**Recommended Starting Point:** Start with `double` - the simplest possible task, then work your way up to more complex examples. - ---- +Expected behavior: -**Test the basic task:** - -Task: `double` - -Input: -```json -5 -``` - -Expected output: `10` - ---- - -**Test subtask calling:** - -Task: `add_doubled_numbers` - -Input: -```json -[3, 4] -``` - -Expected output: -```json -{ - "original_numbers": [3, 4], - "doubled_numbers": [6, 8], - "sum_of_doubled": 14, - "explanation": "3 doubled is 6, 4 doubled is 8, sum is 14" -} -``` - -This task calls `double` twice as subtasks! - ---- - -**Test subtask in a loop:** - -Task: `process_numbers` - -Input: -```json -[1, 2, 3, 4, 5] -``` - -Expected output: -```json -{ - "original_numbers": [1, 2, 3, 4, 5], - "doubled_numbers": [2, 4, 6, 8, 10], - "count": 5, - "explanation": "Processed 5 numbers through the double subtask" -} -``` - -This calls `double` as a subtask 5 times (once for each number)! - ---- - -**Test multi-step workflow:** - -Task: `calculate_and_process` - -Input: -```json -[2, 3, 10, 20, 30] -``` - -This is the most complex example - it calls `add_doubled_numbers` and `process_numbers` as subtasks, which in turn call `double` multiple times. Watch the Render Dashboard to see the entire execution tree! - -## Triggering via SDK - -Once deployed, trigger workflows via the Render Client SDK: - -```python -from render_sdk import Render - -# Uses RENDER_API_KEY environment variable automatically -render = Render() - -# Call the simple double task -task_run = await render.workflows.run_task( - "hello-world-workflows/double", - {"x": 5} -) -result = await task_run -print(f"Result: {result.results}") # Output: 10 - -# Call the subtask orchestration example -task_run = await render.workflows.run_task( - "hello-world-workflows/add_doubled_numbers", - {"a": 3, "b": 4} -) -result = await task_run -print(f"Sum of doubled: {result.results['sum_of_doubled']}") # Output: 14 -``` +- `calculate_square` with `a=5` returns `25` +- `sum_squares` with `a=3,b=4` returns `25` +- `flip_coin` may fail and retry before succeeding -## Tasks Explained - -### `double(x: int) -> int` - -The simplest possible task. Takes a number, doubles it, returns the result. - -**Purpose**: Show what a basic task looks like. - -**Can be called as a subtask**: Yes! Other tasks call this. - ---- - -### `add_doubled_numbers(a: int, b: int) -> dict` - -Demonstrates the fundamental subtask pattern. - -**What it does**: -1. Calls `double(a)` as a subtask -2. Calls `double(b)` as a subtask -3. Adds the results together - -**Purpose**: Show how to call tasks as subtasks using `await`. - -**Key Pattern**: -```python -result = await double(a) # ← Subtask call with await -``` - ---- - -### `process_numbers(numbers: list[int]) -> dict` - -Demonstrates calling a subtask in a loop. - -**What it does**: -1. Takes a list of numbers -2. Calls `double` as a subtask for each number -3. Collects all the results - -**Purpose**: Show how to process lists/batches using subtasks. - -**Key Pattern**: -```python -for num in numbers: - doubled = await double(num) # ← Subtask call in a loop -``` - ---- - -### `calculate_and_process(a: int, b: int, more_numbers: list[int]) -> dict` - -Demonstrates a multi-step workflow with multiple subtask calls. - -**What it does**: -1. Calls `add_doubled_numbers` as a subtask -2. Calls `process_numbers` as a subtask -3. Combines the results +## Deploying to Render -**Purpose**: Show how to chain multiple subtasks to create complex workflows. +Configure your Workflow service with: -**Key Pattern**: -```python -step1 = await add_doubled_numbers(a, b) # ← First subtask -step2 = await process_numbers(numbers) # ← Second subtask -# Combine results -``` +| Option | Value | +| --- | --- | +| Build command | `pip install -r requirements.txt` | +| Start command | `python main.py` | ## Key Concepts -### The `@app.task` Decorator - -Every workflow function needs the `@app.task` decorator: - -```python -from render_sdk import Workflows - -app = Workflows() - -@app.task -def my_task(): - return "Hello World" - -app.start() -``` - -### The `async` Keyword - -Tasks that call other tasks as subtasks must be `async`: +### Task registration -```python -@app.task -async def orchestrator(): - result = await subtask() # ← Calls another task - return result -``` - -### The `await` Keyword - -Use `await` to call a task as a subtask: - -```python -result = await task_name(arguments) -``` - -Without `await`, you're just calling a regular Python function! - -### Task Registration - -All `@app.task` decorated functions are registered when defined. Call `app.start()` at the end of your module to start the workflow service and make all registered tasks available for execution. - -## Common Patterns - -### Pattern 1: Sequential Subtasks - -Execute subtasks one after another: - -```python -@app.task -async def sequential(): - step1 = await task_a() - step2 = await task_b(step1) # Uses result from step1 - step3 = await task_c(step2) # Uses result from step2 - return step3 -``` - -### Pattern 2: Independent Subtasks - -Execute subtasks where order doesn't matter: +Any function decorated with `@app.task` is registered when your service starts via `app.start()`. -```python -@app.task -async def independent(): - result_a = await task_a() - result_b = await task_b() - return combine(result_a, result_b) -``` - -### Pattern 3: Subtasks in a Loop - -Process a list by calling a subtask for each item: - -```python -@app.task -async def batch_process(items: list): - results = [] - for item in items: - result = await process_item(item) - results.append(result) - return results -``` - -### Pattern 4: Nested Subtasks - -Subtasks can call other subtasks: +### Subtasks -```python -@app.task -async def level_1(): - return await level_2() - -@app.task -async def level_2(): - return await level_3() - -@app.task -def level_3(): - return "Done!" -``` - -## Next Steps +Inside an `async` task, calling `await other_task(...)` runs that task as a subtask. -Once you understand this example, check out: +### Retries -1. **ETL Job** - Learn data processing patterns with CSV files -2. **File Processing** - Learn parallel execution with `asyncio.gather()` -3. **Data Pipeline** - Learn complex multi-stage workflows -4. **OpenAI Agent** - Learn advanced patterns with AI integration -5. **File Analyzer** - Learn how to call workflows from APIs using Client SDK +Use `@app.task(retry=Retry(...))` when transient failures should be retried automatically. ## Troubleshooting -### "Task not found" error +### "Task not found" -Make sure: -- The service is deployed and running -- The task name matches exactly (case-sensitive) -- You're using the correct service slug +- Confirm the service is running +- Verify task names exactly match: `calculate_square`, `sum_squares`, `flip_coin` -### Import errors +### Import or dependency issues -Make sure: -- `requirements.txt` includes `render-sdk>=0.5.0` -- Build command is running correctly -- Python version is 3.10 or higher +- Confirm dependency install completed from `requirements.txt` +- Confirm Python version is 3.10+ -### Subtask calls not working +### Retry behavior confusion -Make sure: -- Your task function is marked `async` -- You're using `await` before the task call -- Both tasks are decorated with `@app.task` - -## Important Notes - -- **Python-only**: Workflows are only supported in Python via render-sdk -- **No Blueprint Support**: Workflows don't support render.yaml blueprint configuration -- **Service Type**: Deploy as a Workflow service on Render (not Background Worker or Web Service) -- **Async Functions**: Tasks that call subtasks must be declared as `async` +- Retries only apply to tasks configured with `Retry` +- In this example, only `flip_coin` has retry config ## Resources -- [Render Workflows Documentation](https://docs.render.com/workflows) -- [Render SDK on PyPI](https://pypi.org/project/render-sdk/) -- [Render Dashboard](https://dashboard.render.com/) - ---- - -**Start simple, build powerful workflows!** +- [Render Workflows documentation](https://render.com/docs/workflows) +- [Workflows tutorial](https://render.com/docs/workflows-tutorial) +- [Local development guide](https://render.com/docs/workflows-local-development) diff --git a/hello-world/main.py b/hello-world/main.py index c522110..e088eb0 100644 --- a/hello-world/main.py +++ b/hello-world/main.py @@ -1,195 +1,35 @@ -""" -Hello World - Getting Started with Render Workflows +from render_sdk import Workflows, Retry +import asyncio +import random -This is the simplest possible workflow example to help you understand the basics. -It demonstrates: -- How to define a task using the @app.task decorator -- How to call a task as a subtask using await -- How to orchestrate multiple subtask calls - -No complex business logic - just simple number operations to show the patterns clearly. -""" - -import logging - -from render_sdk import Workflows - -# Configure logging to see what's happening -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -# Initialize Workflows app app = Workflows() -# ============================================================================ -# BASIC TASK - The building block -# ============================================================================ - -@app.task -def double(x: int) -> int: - """ - A basic task that doubles a number. - - This is the simplest possible task - it takes an input, does something, - and returns a result. Tasks are the building blocks of workflows. - - Args: - x: The number to double - - Returns: - The doubled number - """ - logger.info(f"[TASK] Doubling {x}") - result = x * 2 - logger.info(f"[TASK] Result: {result}") - return result - - -# ============================================================================ -# SUBTASK CALLING - Tasks calling other tasks -# ============================================================================ - -@app.task -async def add_doubled_numbers(*args: int) -> dict: - """ - Demonstrates calling a task as a subtask. - - This task calls the 'double' task twice as a subtask using await. - This is the fundamental pattern in Render Workflows - tasks can call - other tasks to break down complex operations into simple, reusable pieces. - - KEY PATTERN: Use 'await task_name(args)' to call a task as a subtask. - - Args: - *args: Two numbers to process - - Returns: - Dictionary with original numbers, doubled values, and their sum - """ - if len(args) != 2: - raise ValueError(f"Expected exactly 2 arguments, got {len(args)}") - - a, b = args - logger.info(f"[WORKFLOW] Starting: add_doubled_numbers({a}, {b})") - - # SUBTASK CALL #1: Call 'double' as a subtask - # The 'await' keyword tells Render to execute this as a subtask - logger.info(f"[WORKFLOW] Calling subtask: double({a})") - doubled_a = await double(a) - - # SUBTASK CALL #2: Call 'double' as a subtask again - logger.info(f"[WORKFLOW] Calling subtask: double({b})") - doubled_b = await double(b) - - # Now we have the results from both subtasks, let's combine them - total = doubled_a + doubled_b - - result = { - "original_numbers": [a, b], - "doubled_numbers": [doubled_a, doubled_b], - "sum_of_doubled": total, - "explanation": f"{a} doubled is {doubled_a}, {b} doubled is {doubled_b}, sum is {total}" - } - - logger.info(f"[WORKFLOW] Complete: {result}") - return result - - -# ============================================================================ -# SUBTASK IN A LOOP - Processing multiple items -# ============================================================================ - @app.task -async def process_numbers(*numbers: int) -> dict: - """ - Demonstrates calling a subtask in a loop. - - This pattern is useful when you need to process multiple items, - and each item requires the same operation (calling the same subtask). - - KEY PATTERN: Call subtasks in a loop to process lists/batches. - - Args: - numbers: List of numbers to process - - Returns: - Dictionary with original numbers and their doubled values - """ - numbers_list = list(numbers) - - logger.info(f"[WORKFLOW] Starting: process_numbers({numbers_list})") - - doubled_results = [] - - # Process each number by calling 'double' as a subtask - for i, num in enumerate(numbers_list, 1): - logger.info(f"[WORKFLOW] Processing item {i}/{len(numbers_list)}: {num}") - - # SUBTASK CALL: Call 'double' as a subtask for each number - doubled = await double(num) - doubled_results.append(doubled) - - result = { - "original_numbers": numbers_list, - "doubled_numbers": doubled_results, - "count": len(numbers_list), - "explanation": f"Processed {len(numbers_list)} numbers through the double subtask" - } +def calculate_square(a: int) -> int: + return a * a - logger.info(f"[WORKFLOW] Complete: {result}") - return result - - -# ============================================================================ -# MULTI-STEP WORKFLOW - Chaining multiple subtasks -# ============================================================================ @app.task -async def calculate_and_process(a: int, b: int, *more_numbers: int) -> dict: - """ - Demonstrates a multi-step workflow that chains multiple subtasks. - - This is a more complex example showing how you can build workflows - that call multiple different subtasks in sequence, using the results - from one subtask as input to the next. - - KEY PATTERN: Chain multiple subtask calls to create complex workflows. - - Args: - a: First number - b: Second number - more_numbers: Additional numbers to process - - Returns: - Dictionary with results from multiple workflow steps - """ - more_numbers_list = list(more_numbers) - - logger.info("[WORKFLOW] Starting multi-step workflow") - - # STEP 1: Add two doubled numbers - logger.info("[WORKFLOW] Step 1: Adding doubled numbers") - step1_result = await add_doubled_numbers(a, b) - - # STEP 2: Process a list of numbers - logger.info("[WORKFLOW] Step 2: Processing number list") - step2_result = await process_numbers(*more_numbers_list) - - # STEP 3: Combine the results - logger.info("[WORKFLOW] Step 3: Combining results") - final_result = { - "step1_sum": step1_result["sum_of_doubled"], - "step2_doubled": step2_result["doubled_numbers"], - "total_operations": 2 + len(more_numbers_list), - "summary": f"Added doubled {a} and {b}, then doubled {len(more_numbers_list)} more numbers" - } - - logger.info("[WORKFLOW] Multi-step workflow complete") - return final_result +async def sum_squares(a: int, b: int) -> int: + result1, result2 = await asyncio.gather( + calculate_square(a), + calculate_square(b), + ) + return result1 + result2 + + +@app.task( + retry=Retry( + max_retries=3, + wait_duration_ms=1000, + backoff_scaling=1.5, + ) +) +def flip_coin() -> str: + if random.random() < 0.5: + raise Exception("Flipped tails! Retrying.") + return "Flipped heads!" if __name__ == "__main__": diff --git a/hello-world/requirements.txt b/hello-world/requirements.txt index 65b4054..03e4930 100644 --- a/hello-world/requirements.txt +++ b/hello-world/requirements.txt @@ -1 +1 @@ -render-sdk>=0.5.0 +render_sdk>=0.6.0 diff --git a/hello-world/template.yaml b/hello-world/template.yaml index 61c8994..53260b4 100644 --- a/hello-world/template.yaml +++ b/hello-world/template.yaml @@ -3,7 +3,7 @@ # after scaffolding. You can safely ignore it when using this example directly. name: Hello World -description: Simple task and subtask basics +description: Minimal Render Workflows starter template default: true workflowsRoot: . @@ -18,8 +18,8 @@ nextSteps: command: render workflows dev -- {{startCommand}} hint: This runs your workflow service locally, allowing you to view and run tasks without deploying to Render. - label: Run a task locally (in another terminal) - command: "render workflows tasks start double --local --input='[5]'" - hint: If you see 10, your tasks are working! + command: "render workflows tasks start calculate_square --local --input='{\"a\": 5}'" + hint: If you see 25, your tasks are working! - label: Deploy your workflow service to Render hint: | To deploy this workflow to Render, first push this code to a remote repository. Then, create a workflow service from your Render Dashboard, connect it to your repository, and provide the following build and start commands: From b4c850375c3d6a324b73d44a5912c463617304c0 Mon Sep 17 00:00:00 2001 From: Stephen Barlow Date: Mon, 6 Apr 2026 16:04:12 -0700 Subject: [PATCH 2/2] Remove retry troubleshooting bit --- hello-world/README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/hello-world/README.md b/hello-world/README.md index 333e48b..e84c0ae 100644 --- a/hello-world/README.md +++ b/hello-world/README.md @@ -107,11 +107,6 @@ Use `@app.task(retry=Retry(...))` when transient failures should be retried auto - Confirm dependency install completed from `requirements.txt` - Confirm Python version is 3.10+ -### Retry behavior confusion - -- Retries only apply to tasks configured with `Retry` -- In this example, only `flip_coin` has retry config - ## Resources - [Render Workflows documentation](https://render.com/docs/workflows)