Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A curated collection of production-ready workflow examples demonstrating various

These examples demonstrate how to build robust, scalable workflows using Render's Python SDK. All examples follow best practices for production deployments and include comprehensive documentation.

Render Workflows are Python-only (via `render-sdk`) and must be deployed as Workflow services on Render.
Render Workflows support both Python and TypeScript. This repo contains Python examples using the `render-sdk` package, deployed as Workflow services on Render.

## Examples

Expand Down
3 changes: 2 additions & 1 deletion data-pipeline/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,4 +494,5 @@ async def run_data_pipeline(user_ids: list[str]) -> dict:
}


app.start()
if __name__ == "__main__":
app.start()
3 changes: 2 additions & 1 deletion etl-job/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,4 +335,5 @@ async def run_etl_pipeline(source_file: str) -> dict:
}


app.start()
if __name__ == "__main__":
app.start()
35 changes: 17 additions & 18 deletions file-analyzer/api-service/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from typing import Any
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from render_sdk import Render
from render_sdk import RenderAsync
from render_sdk.client.errors import RenderError, TaskRunError
from pydantic import BaseModel

Expand Down Expand Up @@ -62,9 +62,9 @@ class HealthResponse(BaseModel):


# Client SDK helper functions
def get_client() -> Render:
def get_client() -> RenderAsync:
"""
Get Render API client.
Get async Render API client.

The Client SDK requires a RENDER_API_KEY environment variable.
Get your API key from: Render Dashboard → Account Settings → API Keys
Expand All @@ -76,7 +76,7 @@ def get_client() -> Render:
status_code=500,
detail="RENDER_API_KEY not configured. Get your API key from Render Dashboard → Account Settings → API Keys"
)
return Render() # Uses RENDER_API_KEY env var automatically
return RenderAsync() # Uses RENDER_API_KEY env var automatically


def get_task_identifier(task_name: str) -> str:
Expand Down Expand Up @@ -191,26 +191,25 @@ async def analyze_file(file: UploadFile = File(...)):

logger.info(f"Calling workflow task: {task_identifier}")

# CLIENT SDK CALL: Run the workflow task
# Format: client.workflows.run_task(task_identifier, {"arg": value})
task_run = await client.workflows.run_task(
# CLIENT SDK CALL: Start the workflow task
# start_task returns an AwaitableTaskRun immediately
started_run = await client.workflows.start_task(
task_identifier,
{"file_content": file_content_str}
)

logger.info(f"Task started: {task_run.id}")
logger.info(f"Task started: {started_run.id}")

# CLIENT SDK CALL: Await the task completion
# This will block until the task finishes
result = await task_run
# Await the AwaitableTaskRun to wait for completion
result = await started_run

logger.info(f"Task completed with status: {result.status}")

return AnalysisResponse(
task_run_id=result.id,
status=result.status,
message=f"File '{file.filename}' analyzed successfully",
result=result.results # Task return value
result=result.results
)

except TaskRunError as e:
Expand Down Expand Up @@ -281,16 +280,16 @@ async def analyze_with_custom_task(task_name: str, file: UploadFile = File(...))

logger.info(f"Calling workflow task: {task_identifier}")

# CLIENT SDK CALL: Run the specified workflow task
task_run = await client.workflows.run_task(
# CLIENT SDK CALL: Start the specified workflow task
started_run = await client.workflows.start_task(
task_identifier,
{"file_content": file_content_str}
)

logger.info(f"Task started: {task_run.id}")
logger.info(f"Task started: {started_run.id}")

# CLIENT SDK CALL: Await the task completion
result = await task_run
# Await the AwaitableTaskRun to wait for completion
result = await started_run

logger.info(f"Task '{task_name}' completed with status: {result.status}")

Expand Down Expand Up @@ -325,7 +324,7 @@ async def analyze_with_custom_task(task_name: str, file: UploadFile = File(...))
import uvicorn

logger.info("Starting File Analyzer API Service")
logger.info("This service calls workflow tasks using the Client SDK")
logger.info("This service calls workflow tasks using the async Client SDK (RenderAsync)")
logger.info("Required environment variables:")
logger.info(" - RENDER_API_KEY: Your Render API key")
logger.info(" - WORKFLOW_SERVICE_SLUG: Your workflow service slug (e.g., 'file-analyzer-workflows')")
Expand Down
3 changes: 2 additions & 1 deletion file-analyzer/workflow-service/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,4 +327,5 @@ async def analyze_file(file_content: str) -> dict:
}


app.start()
if __name__ == "__main__":
app.start()
6 changes: 2 additions & 4 deletions file-processing/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,6 @@ async def process_file_batch(*file_paths: str) -> dict:
Returns:
Dictionary with results for all files
"""
# Convert to list for easier handling
file_paths_list = list(file_paths)

logger.info("=" * 80)
Expand All @@ -421,9 +420,7 @@ async def process_file_batch(*file_paths: str) -> dict:
# Process all files in parallel
# SUBTASK PATTERN: Call multiple subtasks concurrently using asyncio.gather()
logger.info("[BATCH] Launching parallel file processing tasks...")
# Create list of subtask calls (one per file)
tasks = [process_single_file(fp) for fp in file_paths_list]
# SUBTASK CALLS: Execute all process_single_file subtasks in parallel
results = await asyncio.gather(*tasks)

# Aggregate results
Expand Down Expand Up @@ -513,4 +510,5 @@ async def generate_consolidated_report(batch_result: dict) -> dict:
return report


app.start()
if __name__ == "__main__":
app.start()
17 changes: 2 additions & 15 deletions hello-world/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,6 @@ def double(x: int) -> int:
Returns:
The doubled number
"""
# Handle case where x might be a dictionary instead of an integer
if isinstance(x, dict):
if 'x' in x:
x = x['x']
logger.info(f"[TASK] Extracted x from dictionary: {x}")
else:
logger.error(f"[TASK] Dictionary input missing 'x' key: {x}")
raise ValueError(f"Expected integer or dict with 'x' key, got: {x}")
elif not isinstance(x, int):
logger.error(f"[TASK] Invalid input type: {type(x)}, value: {x}")
raise ValueError(f"Expected integer, got: {type(x)}")

logger.info(f"[TASK] Doubling {x}")
result = x * 2
logger.info(f"[TASK] Result: {result}")
Expand Down Expand Up @@ -131,7 +119,6 @@ async def process_numbers(*numbers: int) -> dict:
Returns:
Dictionary with original numbers and their doubled values
"""
# Convert to list for easier handling
numbers_list = list(numbers)

logger.info(f"[WORKFLOW] Starting: process_numbers({numbers_list})")
Expand Down Expand Up @@ -180,7 +167,6 @@ async def calculate_and_process(a: int, b: int, *more_numbers: int) -> dict:
Returns:
Dictionary with results from multiple workflow steps
"""
# Convert to list for easier handling
more_numbers_list = list(more_numbers)

logger.info("[WORKFLOW] Starting multi-step workflow")
Expand All @@ -206,4 +192,5 @@ async def calculate_and_process(a: int, b: int, *more_numbers: int) -> dict:
return final_result


app.start()
if __name__ == "__main__":
app.start()
4 changes: 2 additions & 2 deletions openai-agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,6 @@ async def multi_turn_conversation(*messages: str) -> dict:
Returns:
Dictionary with full conversation and all responses
"""
# Convert to list for easier handling
messages_list = list(messages)

logger.info("=" * 80)
Expand Down Expand Up @@ -522,4 +521,5 @@ async def multi_turn_conversation(*messages: str) -> dict:
}


app.start()
if __name__ == "__main__":
app.start()