diff --git a/skills/render-workflows/SKILL.md b/skills/render-workflows/SKILL.md index 137222b..666dec6 100644 --- a/skills/render-workflows/SKILL.md +++ b/skills/render-workflows/SKILL.md @@ -1,145 +1,179 @@ --- name: render-workflows -description: Sets up, develops, tests, and deploys Render Workflows. Covers first-time scaffolding (via CLI or manual), SDK installation (Python or TypeScript), task patterns (retries, subtasks, fan-out), local development, Dashboard deployment, and troubleshooting. Use when a user wants to set up Render Workflows for the first time, scaffold a workflow service, add or modify workflow tasks, test workflows locally, or deploy workflows to Render. +description: Build, validate locally, deploy, and troubleshoot Render Workflows with the current Python or TypeScript SDK. Use for defining or chaining tasks, integrating the SDK into an existing project, running tasks with the local development server, optionally scaffolding a starter, triggering runs from application code, and releasing workflows. license: MIT -compatibility: Requires Render CLI 2.11.0+ for scaffolding and local development. Render Dashboard required for deployment (Blueprints not yet supported for Workflows). +compatibility: Requires Render SDK 1.x. Local validation requires Render CLI 2.12.0+; optional scaffolding and CLI deployment require Render CLI 2.16.0+. metadata: author: Render - version: "1.0.0" + version: "1.1.3" category: workflows --- # Render Workflows -Render Workflows rapidly distribute computational work across multiple independent instances. -Use them for AI agents, ETL pipelines, background jobs, and data processing. +Render Workflows is an SDK-based product for orchestrating long-running distributed tasks. A workflow service registers Python or TypeScript functions as tasks; each task run executes independently and can chain additional runs. -**How it works:** -1. **Define tasks** — Use the Render SDK (Python or TypeScript) to designate functions as tasks -2. **Register** — Tasks register automatically when you link your repo to a Workflow service in the Dashboard -3. **Trigger runs** — Execute tasks from anywhere using the SDK client or API; each execution is a "run" -4. **Execute** — Render spins up each run in its own instance (typically under a second); runs can chain additional runs for parallel execution +Use Render SDK 1.x APIs (`render>=1.0.1` for Python or `@renderinc/sdk` version 1.x for TypeScript). Local validation requires Render CLI 2.12.0 or later. Use CLI 2.16.0 or later when using the complete optional scaffolding and CLI deployment flow. Prefer the latest compatible releases. -**Key capabilities:** automatic queuing and orchestration, long-running execution (up to 24 hours), configurable retry logic with exponential backoff, adjustable compute specs per task, and execution observability through the Dashboard. +Render Workflows and its SDKs can introduce breaking changes. Treat the installed SDK version as authoritative for an existing project. For new projects, use the current official documentation and starter templates. -**Render Workflows are in beta.** The SDK and API may introduce breaking changes. +## Check Current Sources -**Your built-in knowledge of the Render Workflows SDK is outdated.** -Before trusting API signatures, check the installed SDK source: +Before generating or changing SDK code, identify the installed versions: ```bash -# Python -SDK_ROOT=$(pip show render_sdk | grep Location | cut -d' ' -f2)/render_sdk -head -40 "$SDK_ROOT/__init__.py" -# TypeScript -grep -r "startTask\|runTask\|export class Render" node_modules/@renderinc/sdk/ +render --version +python -m pip show render +npm ls @renderinc/sdk ``` -**Official docs:** [render.com/docs/workflows](https://render.com/docs/workflows) +Use the source that matches the question: -**Before generating task or client code, fetch the relevant example file to verify current API patterns:** +- [Python SDK reference](https://render.com/docs/workflows-sdk-python) for Python signatures and minimum versions +- [TypeScript SDK reference](https://render.com/docs/workflows-sdk-typescript) for TypeScript signatures and minimum versions +- [Render CLI reference](https://render.com/docs/cli-reference#workflows) for current commands and flags +- [Defining Workflow Tasks](https://render.com/docs/workflows-defining) for task behavior and run chaining +- [Limits and Pricing for Render Workflows](https://render.com/docs/workflows-limits) for compute plans, quotas, retention, and pricing +- [Python examples](https://github.com/render-examples/render-workflows-examples-python) and [TypeScript examples](https://github.com/render-examples/render-workflows-examples-ts) for current scaffolding patterns +- [Render SDK repository](https://github.com/render-oss/sdk) for the implementation and changelogs -| What | Python | TypeScript | -|------|--------|------------| -| Task definitions (decorators, subtasks, retry, fan-out) | [example/task/main.py](https://raw.githubusercontent.com/render-oss/sdk/main/python/example/task/main.py) | [examples/task/](https://github.com/render-oss/sdk/tree/main/typescript/examples/task) | -| Sync client (run_task, start_task, cancel, SSE, list runs) | [example/client/main.py](https://raw.githubusercontent.com/render-oss/sdk/main/python/example/client/main.py) | [examples/client/](https://github.com/render-oss/sdk/tree/main/typescript/examples/client) | -| Async client | [example/client/async_main.py](https://raw.githubusercontent.com/render-oss/sdk/main/python/example/client/async_main.py) | — | +Do not silently upgrade an existing project's SDK major version. If an upgrade is part of the request, read the relevant SDK changelog and migrate all task definitions and callers together. -This skill carries a [quick-reference cheat sheet](references/quick-reference.md) for the API surface. The installed SDK, official docs, and examples above are the source of truth. +## SDK 1.x Invariants ---- +Preserve these rules in generated code: + +- Python installs and imports the `render` package, not `render_sdk`. +- Every task function accepts a `TaskContext` as its first positional parameter. Render supplies it; callers do not include it in task input. +- Chain a task run with `await ctx.run(task_definition, ...args)`. A task definition is not directly callable. +- All tasks in a `ctx.run` chain belong to the same workflow service. Trigger another workflow through the SDK client or Render API. +- Python calls `app.start()` from its workflow entrypoint. TypeScript task registration auto-starts in the workflow environment. +- Task arguments and return values must be JSON-serializable. -## Getting Started +Read [references/quick-reference.md](references/quick-reference.md) when writing SDK code. Read [references/task-patterns.md](references/task-patterns.md) for chaining, fan-out, retries, scheduled triggers, or cross-workflow calls. -Supported languages: **Python** and **TypeScript**. +## Choose a Starting Point -### Prerequisites +Do not require `render workflows init`. In an existing codebase, add the SDK and task definitions directly, preserve the project's dependency and build conventions, and validate them with the local development server. For a minimal project or direct integration guidance, read [references/manual-scaffolding.md](references/manual-scaffolding.md). -**Render CLI (required)** +### Optional starter scaffolding + +Use the CLI scaffolder when the user wants a quick example project or a new standalone workflow service: ```bash -render --version +render workflows init ``` -Requires version 2.11.0+. If not installed: -- macOS: `brew install render` -- Linux/macOS: `curl -fsSL https://raw.githubusercontent.com/render-oss/cli/main/bin/install.sh | sh` -- Windows: download the executable from the [CLI releases page](https://github.com/render-oss/cli/releases/) +For non-interactive setup, pass the language and destination explicitly: + +```bash +render workflows init --confirm --language python --dir workflows --git=false +render workflows init --confirm --language node --dir workflows --git=false +``` + +Use `--git=false` when scaffolding inside an existing Git repository to avoid creating a nested repository. Other useful options include `--template`, `--install-deps`, and `--install-agent-skill`; verify current behavior in the CLI reference. + +## Try It Locally with the SDK -### Scaffold a new workflow service +For a first integration, follow the runnable Python or TypeScript [local SDK walkthrough](references/manual-scaffolding.md#try-it-locally-with-the-sdk): define `ping`, start the local task server, invoke it from a separate client script, and verify the returned `"pong"`. No `init`, deployment, or Render API key is needed for this local example. Set local mode in the calling application process, not only in the task server's environment. -**Always prefer `render workflows init` as the primary setup path.** Only fall back to manual scaffolding if the CLI command is unavailable. +After adding or changing SDK task definitions, use the local development server as the primary validation loop. Start it with the workflow's actual start command: ```bash -render workflows init +render workflows dev -- ``` -**Interactive mode** (default): walks the user through scaffolding an example project, testing it locally, and deploying it to Render. +In another terminal, list and run tasks against the local server: -**Non-interactive mode**: sets up an example project without prompting. +```bash +render workflows tasks list --local +render workflows start --local --input='[]' -o json +render workflows tasks runs show --local -o json +``` -If `render workflows init` fails or is not available: -- **Command not found:** CLI version may be too old. Run `render --version` and upgrade to 2.11.0+. -- **Command not supported:** fall back to [references/manual-scaffolding.md](references/manual-scaffolding.md) for step-by-step manual setup. +Use a JSON array for positional input. Python tasks can also receive a JSON object for named input. Read [references/local-development.md](references/local-development.md) for environment files, custom ports, run inspection, cancellation, and application-client configuration. ---- +In non-interactive mode, `workflows start` returns the new task run ID before the run necessarily finishes. Use that ID with `tasks runs show`, polling for a bounded period until the run completes, fails, or is canceled. Verify the result or error, not only that the run was created. -## Define Tasks +When safe and practical, run this verification yourself and stop the dev server afterward. Do not claim a task registers successfully based only on source inspection. -Guide the user through defining their actual tasks. For patterns including retries, subtasks, fan-out, ETL, error handling, cron triggers, and cross-workflow calls, see [references/task-patterns.md](references/task-patterns.md). +## Deploy After Local Validation + +Creating or releasing a workflow changes the user's Render account. Only do it when deployment is part of the request and the tasks have been validated locally when local execution is supported. + +Before deploying, authenticate and confirm the target workspace: -**After adding a task**, verify it registers by starting the local dev server and listing tasks: ```bash -render workflows dev -- -# In another terminal: -render workflows tasks list --local +render whoami +render workspace current +``` + +If authentication is missing, use `render login` for an interactive local session or provide `RENDER_API_KEY` through the environment for automation. Never print the key. + +Confirm the intended branch and region, and ensure the exact code to deploy is committed and pushed to GitHub, GitLab, or Bitbucket. `--repo .` resolves the repository's remote; it does not deploy uncommitted local changes. + +Create the workflow with the CLI. Interactive mode prompts for configuration: + +```bash +render workflows create ``` -If the task doesn't appear, see [Troubleshooting > Task Registration Issues](references/troubleshooting.md#task-registration-issues). +For a non-interactive deployment, use the build and run commands from the selected starter template or the project's existing configuration: -## Local Development +```bash +render workflows create \ + --name my-workflow \ + --repo . \ + --branch \ + --region \ + --runtime python \ + --build-command "pip install -r requirements.txt" \ + --run-command "python main.py" +``` -See [references/local-development.md](references/local-development.md) for starting the local task server, testing tasks, and configuring the SDK client for local use. +For a workflow in a subdirectory, add `--root-directory`. Environment variables can be supplied with repeatable `--env-file` and `--env-var` flags. Never expose secrets in commands or output. -## Deploy to Render +The Render Dashboard is also a supported creation path and is currently required for Docker-based workflow creation. Before recommending a Blueprint, check the current [Workflows limitations](https://render.com/docs/workflows#faq) and [Blueprint specification](https://render.com/docs/blueprint-spec); support is evolving. -Workflows are deployed as a **Workflow** service type in the Render Dashboard. **Blueprints (render.yaml) are not yet compatible with Workflows.** +For later explicit releases, use: -**Deploy checklist:** +```bash +render workflows versions release \ + --commit \ + --wait +``` + +Do not report deployment success from command acceptance alone. Wait for the release, then verify that the expected tasks registered and that one safe task reaches a terminal state with the expected result: -- [ ] Code pushed to GitHub, GitLab, or Bitbucket -- [ ] In the [Render Dashboard](https://dashboard.render.com), click **New > Workflow** -- [ ] Link your repository -- [ ] Set **Root Directory** to `workflows/` -- [ ] Configure build and start commands (see table below) -- [ ] Add environment variables (e.g., `RENDER_API_KEY` for tasks that call other workflows) -- [ ] Click **Deploy Workflow** -- [ ] Verify deployment: check the Dashboard for a successful deploy event +```bash +render workflows versions list -o text +render workflows tasks list -o text +render workflows start / --input='[]' -o json +render workflows tasks runs show -o json +``` -| Field | Python | TypeScript | -|-------|--------|------------| -| **Language** | Python 3 | Node | -| **Build Command** | `pip install -r requirements.txt` | `npm install && npm run build` | -| **Start Command** | `python main.py` | `node dist/main.js` | +Poll `tasks runs show` for a bounded period if the run is still queued or running. Stop and report the release or task error instead of retrying an external mutation indefinitely. -If the deploy fails, check the service logs in the Dashboard. For common deployment errors, see [Troubleshooting](references/troubleshooting.md). For general deploy debugging, use the **render-debug** skill. +If deployment fails, read [references/troubleshooting.md](references/troubleshooting.md). Use the `render-debug` skill for broader Render deployment diagnosis when it is available. -**Running tasks from other services:** +## Trigger Deployed Tasks -After deployment, trigger tasks from your other Render services using the SDK client. +Task slugs use `{workflow-slug}/{task-name}`. + +Python, synchronous: -Python (synchronous): ```python -from render_sdk import Render +from render import Render render = Render() result = render.workflows.run_task("my-workflow/hello", ["world"]) print(result.results) ``` -Python (asynchronous): +Python, asynchronous: + ```python -from render_sdk import RenderAsync +from render import RenderAsync render = RenderAsync() started = await render.workflows.start_task("my-workflow/hello", ["world"]) @@ -148,6 +182,7 @@ print(finished.results) ``` TypeScript: + ```typescript import { Render } from "@renderinc/sdk"; @@ -157,54 +192,26 @@ const finished = await started.get(); console.log(finished.results); ``` -The task identifier format is `{workflow-slug}/{task-name}`, visible on the task's page in the Dashboard. - -Workflows do not have built-in scheduling. To trigger tasks on a schedule, use a Render cron job with the SDK client. For cron and cross-workflow examples, see [references/task-patterns.md](references/task-patterns.md). - ---- - -## Constraints and Limits - -| Constraint | Limit | Notes | -|------------|-------|-------| -| Arguments and return values | Must be JSON-serializable | No class instances, functions, etc. | -| Argument size | 4 MB max | Per task invocation | -| Task definitions | 500 per workflow service | | -| Concurrent runs | 20-100 base (plan-dependent) | Max 200-300 with purchased concurrency | -| Timeout range | 30-86,400 seconds | Default: 2 hours (7,200s) | -| Run duration | Up to 24 hours | | +These clients use `RENDER_API_KEY` unless a token is passed explicitly. Prefer environment variables or a secret manager over hard-coded credentials. -### Instance Types +Workflows do not currently provide native scheduled triggers. Use a Render cron job that invokes the SDK client or API when scheduling is required. -| Plan | Specs | -|------|-------| -| `starter` | 0.5 CPU / 512 MB | -| `standard` (default) | 1 CPU / 2 GB | -| `pro` | 2 CPU / 4 GB | -| `pro_plus` | 4 CPU / 8 GB | -| `pro_max` | 4 CPU / 16 GB | -| `pro_ultra` | 8 CPU / 32 GB | +## Limits, Compute Plans, and Pricing -`pro_plus`, `pro_max`, and `pro_ultra` require requesting access. Set via the `plan` task option. +Do not hard-code compute-plan IDs, resource specifications, quotas, retention periods, or prices in this skill or its references. -Workflow runs incur an additional cost. Confirm current details at [Render pricing](https://render.com/pricing), and see [Render Workflows limits](https://render.com/docs/workflows-limits) for resource constraints. - ---- +Before setting a task's `plan`, estimating cost, or advising on capacity, consult [Limits and Pricing for Render Workflows](https://render.com/docs/workflows-limits). Keep only API-shape constraints needed to write correct code in the skill. ## References -- **Quick-reference cheat sheet:** [references/quick-reference.md](references/quick-reference.md) (API surface, env vars, error types) -- **Task patterns:** [references/task-patterns.md](references/task-patterns.md) -- **Local development:** [references/local-development.md](references/local-development.md) -- **Troubleshooting:** [references/troubleshooting.md](references/troubleshooting.md) -- **Manual scaffolding (fallback):** [references/manual-scaffolding.md](references/manual-scaffolding.md) -- **Official docs:** [render.com/docs/workflows](https://render.com/docs/workflows) -- **Starter template (Python):** [render-examples/workflows-template-python](https://github.com/render-examples/workflows-template-python) -- **Starter template (TypeScript):** [render-examples/workflows-template-ts](https://github.com/render-examples/workflows-template-ts) -- **SDK repo:** [github.com/render-oss/sdk](https://github.com/render-oss/sdk) +- [references/quick-reference.md](references/quick-reference.md): SDK 1.x task and client surface +- [references/task-patterns.md](references/task-patterns.md): chaining, fan-out, retries, cron triggers, and cross-workflow calls +- [references/local-development.md](references/local-development.md): local server, CLI runs, environment configuration, and limitations +- [references/troubleshooting.md](references/troubleshooting.md): common setup, registration, execution, and client failures +- [references/manual-scaffolding.md](references/manual-scaffolding.md): direct SDK project setup without `workflows init` ## Related Skills -- **render-deploy:** Deploy web services, static sites, and databases -- **render-debug:** Debug failed deployments and runtime errors -- **render-monitor:** Monitor service health and performance +- `render-deploy`: deploy other Render service types +- `render-debug`: diagnose deployment and runtime failures +- `render-monitor`: inspect service health and performance diff --git a/skills/render-workflows/references/local-development.md b/skills/render-workflows/references/local-development.md index 46fbf80..4e32c41 100644 --- a/skills/render-workflows/references/local-development.md +++ b/skills/render-workflows/references/local-development.md @@ -1,116 +1,106 @@ # Local Development -Run workflow tasks locally for faster development and testing. - -## Contents - -- Prerequisites -- Starting the local task server -- Triggering task runs (CLI and application code) -- Viewing results -- Limitations +Use the Render CLI's local task server to register and run tasks without deploying. Read the current [local development documentation](https://render.com/docs/workflows-local-development) when behavior differs from this reference. ## Prerequisites -- **Render CLI 2.11.0+**: `render --version` - - macOS: `brew install render` - - Linux/macOS: `curl -fsSL https://raw.githubusercontent.com/render-oss/cli/main/bin/install.sh | sh` - - Windows: download the executable from the [CLI releases page](https://github.com/render-oss/cli/releases/) -- A workflow project with registered tasks +- Render CLI 2.12.0 or later for local development; this skill recommends 2.16.0 or later for the complete scaffolding and deployment flow +- A Python or TypeScript workflow project with its dependencies installed +- A non-Docker workflow start command; Docker-based workflows do not currently support the local task server -## Starting the Local Task Server +## Start the Local Task Server -From your project directory: +Run from the workflow project's root directory: ```bash # Python -render workflows dev -- python workflows/main.py +render workflows dev -- python main.py -# TypeScript -render workflows dev -- npx tsx workflows/main.ts +# TypeScript starter +render workflows dev -- npm start ``` -The local server starts on port `8120`. Customize with `--port`: +The default port is `8120`. Use the same custom port on every related CLI command: ```bash -render workflows dev --port 8121 -- python workflows/main.py +render workflows dev --port 8121 -- python main.py +render workflows tasks list --local --port 8121 ``` -The server picks up code changes automatically as you iterate. +The CLI loads `.env` from the current directory automatically. To load explicit files, pass `--env-file` more than once if needed; later files override earlier files: + +```bash +render workflows dev \ + --env-file .env \ + --env-file .env.local \ + -- python main.py +``` -## Triggering Task Runs +Use `--debug` when task execution events are needed for diagnosis. -### From the CLI +## Run and Inspect Tasks -List and run tasks interactively: +Interactive task browser: ```bash render workflows tasks list --local ``` -**The `--local` flag is required.** Without it, the CLI lists deployed (remote) tasks. +Non-interactive flow: -The interactive menu lets you: -1. Select a task -2. Choose `run` -3. Provide input as a JSON array (e.g., `[5]` or `[]`) -4. View live logs +```bash +render workflows tasks list --local -o text +render workflows start calculate_square --local --input='[5]' -o text +render workflows tasks runs list calculate_square --local -o text +render workflows tasks runs show --local -o json +render workflows cancel --local +``` -### From Application Code +Use `-o text`, `-o json`, or `-o yaml` to disable menu navigation. The canonical long form of `render workflows start` is `render workflows tasks runs start`; the canonical long form of `cancel` is `render workflows tasks runs cancel`. -Configure your app to target the local task server. +Non-interactive `workflows start` can return while the run is still queued or running. Copy its task run ID and poll `tasks runs show` for a bounded period until the status is completed, failed, or canceled. Confirm the result or error rather than treating run creation as successful execution. -**Python:** +Inputs: -Set environment variables: -```bash -RENDER_USE_LOCAL_DEV=true -``` +- Use a JSON array for positional arguments: `[5]`, `["left", "right"]`, or `[]`. +- A Python task can use a JSON object for named arguments: `{"value": 5}`. +- Do not include the `TaskContext` parameter in either input form. + +## Trigger Local Runs from Application Code + +For complete Python and TypeScript client scripts with a verified `ping` result, follow [Try It Locally with the SDK](manual-scaffolding.md#try-it-locally-with-the-sdk). + +Local SDK calls do not require a Render API key or a deployed workflow. Set local mode in the **calling application's** environment (setting it only on the task server does not configure a separately running client): -Or for a custom port: ```bash -RENDER_USE_LOCAL_DEV=true -RENDER_LOCAL_DEV_URL=http://localhost:8121 +export RENDER_USE_LOCAL_DEV=true ``` -The SDK clients (`Render()` and `RenderAsync()`) automatically detect these and route requests to the local server. - -**TypeScript:** +For a custom server URL, also set: -Same environment variables: ```bash -RENDER_USE_LOCAL_DEV=true +export RENDER_LOCAL_DEV_URL=http://localhost:8121 ``` -Or pass configuration directly: +Python `Render()` and `RenderAsync()` clients detect these variables. TypeScript supports the same variables or explicit configuration: + ```typescript import { Render } from "@renderinc/sdk"; const render = new Render({ useLocalDev: true, - localDevUrl: "http://localhost:8120", + localDevUrl: "http://localhost:8121", }); ``` -**Render API (any language):** - -Swap the base URL for task endpoints: -``` -http://localhost:8120 -``` - -The local task server only simulates task-related endpoints. Other Render API endpoints are not supported locally. - -## Viewing Results +For direct Render API code, use the local task server as the base URL for task endpoints only. Other Render API endpoints are not simulated. -After running a task via the CLI: -1. Press **Esc** to go back to the command menu -2. Select `runs` to see task runs -3. Select a run and choose `results` to see output +## Local-Only Behavior -## Limitations +- Logs and results are held in memory and disappear when the server stops. +- Stored run history can increase memory usage; restart the server during high-volume testing. +- Local task and run identifiers are generated for each server session and do not correspond to deployed identifiers. +- The server reloads task definitions when it starts subprocesses, so source changes are picked up without deploying. +- Local execution simulates task orchestration but does not reproduce production container isolation, networking, or compute characteristics. -- Logs and results are stored **in memory** and lost on server shutdown -- High volume of runs can increase memory usage; restart the server periodically -- Task and run IDs are random UUIDs, not matching deployed identifiers -- Subtasks run locally in the same server process +Stop the local task server after verification when an agent started it for the user. diff --git a/skills/render-workflows/references/manual-scaffolding.md b/skills/render-workflows/references/manual-scaffolding.md index e3faafa..2fe957e 100644 --- a/skills/render-workflows/references/manual-scaffolding.md +++ b/skills/render-workflows/references/manual-scaffolding.md @@ -1,129 +1,208 @@ -# Manual Scaffolding (Fallback) +# Direct SDK Project Setup -Use this path only if `render workflows init` is not available. Follow these steps in order. +Use this reference to add the Render Workflows SDK directly to an existing codebase or create a minimal workflow service without `render workflows init`. Preserve an existing project's dependency, module, and build conventions. For a new standalone service, prefer adapting the current official [Python examples](https://github.com/render-examples/render-workflows-examples-python) or [TypeScript examples](https://github.com/render-examples/render-workflows-examples-ts) over copying this minimal structure blindly. -## Contents +## Choose the Language and Boundary -- Step 1: Detect language -- Step 2: Create the `workflows/` directory -- Step 3: Install dependencies -- Step 4: Verify setup +Use the language requested by the user. Otherwise infer it from the project: -> **IMPORTANT:** Do NOT modify the project's root `package.json` or `requirements.txt`. Do NOT run `npm install ` or `pip install ` at the project root. The `workflows/` directory is a self-contained service with its own dependency files. +| Indicators | Language | +|---|---| +| `pyproject.toml`, `requirements.txt`, `Pipfile`, or Python source | Python | +| `package.json`, `tsconfig.json`, or TypeScript source | TypeScript | -> **The official starter templates have likely changed since this skill was written.** -> Always check the real template before scaffolding: -> - **Python:** [render-examples/workflows-template-python](https://github.com/render-examples/workflows-template-python) -> -> If the user already has the SDK installed, inspect it for up-to-date signatures: -> ```bash -> # Python: check SDK source -> SDK_ROOT=$(pip show render_sdk | grep Location | cut -d' ' -f2)/render_sdk -> head -40 "$SDK_ROOT/__init__.py" -> -> # TypeScript: check type definitions -> grep -r "export.*task\|export.*Render" node_modules/@renderinc/sdk/ -> ``` -> -> **Official examples:** [Python](https://github.com/render-oss/sdk/tree/main/python/example) | [TypeScript](https://github.com/render-oss/sdk/tree/main/typescript/examples) -> -> The inline snippets in this skill are a fallback. The official repos are the source of truth. +If both ecosystems are present, prefer the language already used by the component that will trigger or own the workflow. Ask only when the choice materially changes the integration. -## Step 1: Detect Language +Create a self-contained workflow service directory when the workflow has an independent build or deployment boundary. Do not overwrite unrelated root dependency files. -**Principle:** If the user named the language in their prompt, use it directly. Auto-detect from config files next. Only ask if genuinely ambiguous. +In an existing application, add a dedicated workflow entrypoint and run it separately from the web server. Keep task registration out of the client script and preserve the application's start command; add a script such as `workflows:start` for the workflow process. -Check the project for language indicators: +## Python -| Indicator | Language | -|-----------|----------| -| `requirements.txt`, `pyproject.toml`, `Pipfile`, `*.py` | Python | -| `package.json`, `tsconfig.json`, `*.ts` | TypeScript | +Minimum files: -If both are present or neither is found, ask the user which language to use. - -## Step 2: Create the `workflows/` Directory - -Use the official Render starter templates as the source of truth for file structure and contents: - -- **Python:** [render-examples/workflows-template-python](https://github.com/render-examples/workflows-template-python) -- **TypeScript:** [render-examples/workflows-template-ts](https://github.com/render-examples/workflows-template-ts) +```text +workflows/ +|-- main.py +`-- requirements.txt +``` -Fetch the template contents (clone, download, or read from the repo) and place them in a `workflows/` directory at the project root. At minimum, the directory should contain: +`requirements.txt`: -**Python:** `main.py`, `requirements.txt` -**TypeScript:** `index.ts` (or `main.ts`), `package.json`, `tsconfig.json` +```text +render>=1.0.1 +``` -Key patterns to follow from the templates: +`main.py`: -**Python entry point** (`main.py`): ```python -from render_sdk import Workflows, Retry +from render import TaskContext, Workflows -app = Workflows( - default_retry=Retry(max_retries=3, wait_duration_ms=1000, backoff_scaling=2.0), -) +app = Workflows() @app.task -def ping() -> str: +def ping(_ctx: TaskContext) -> str: return "pong" if __name__ == "__main__": app.start() ``` -**TypeScript entry point** (`index.ts`): +Install dependencies in an isolated environment: + +```bash +cd workflows +python3 -m venv .venv +source .venv/bin/activate +python -m pip install -r requirements.txt +``` + +Use an equivalent activation command on Windows or non-POSIX shells. + +## TypeScript + +Minimum files: + +```text +workflows/ +|-- src/ +| `-- main.ts +|-- package.json +`-- tsconfig.json +``` + +`package.json`: + +```json +{ + "name": "my-workflow", + "private": true, + "type": "module", + "scripts": { + "workflows:start": "tsx src/main.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@renderinc/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "tsx": "^4.20.2", + "typescript": "^5.0.0" + } +} +``` + +`src/main.ts`: + ```typescript -import { task } from "@renderinc/sdk/workflows"; +import { task, type TaskContext } from "@renderinc/sdk/workflows"; task( - { - name: "ping", - retry: { maxRetries: 3, waitDurationMs: 1000, backoffScaling: 2.0 }, - }, - function ping(): string { + { name: "ping" }, + function ping(_ctx: TaskContext): string { return "pong"; }, ); ``` -Each template includes a zero-argument `ping` task so the user can immediately verify the setup works. +For this standalone example, use Node.js 20 or later and this `tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} +``` + +Install dependencies and check the TypeScript source: -## Step 3: Install Dependencies +```bash +cd workflows +npm install +npm run typecheck +``` -After all files from Step 2 are created, install dependencies. +When integrating into an established service, preserve its TypeScript configuration and use Node type definitions compatible with its runtime. Adapt the dedicated workflow script to its existing tooling. + +## Try It Locally with the SDK + +Use the `ping` task and dependencies defined above for your chosen language. No deployment, Render API key, or `render workflows init` is needed. The task server and calling script run in separate processes. + +### Python + +Add `client.py` next to `main.py`: + +```python +from render import Render + +render = Render() +result = render.workflows.run_task("ping", []) +assert result.results == ["pong"], result +print(result.results[0]) +``` + +In terminal 1, from the workflow directory, start the task server: -**Python:** ```bash -pip install -r workflows/requirements.txt +render workflows dev -- .venv/bin/python main.py ``` -**TypeScript:** +In terminal 2, from the same directory, enable local mode for the **client process** and invoke the task: + ```bash -npm install --prefix workflows +RENDER_USE_LOCAL_DEV=true .venv/bin/python client.py ``` -## Step 4: Verify Setup +### TypeScript -Check that setup succeeded before handing off: +Add `src/client.ts`: -- [ ] `workflows/` directory exists with the expected files -- [ ] Dependencies installed without errors +```typescript +import assert from "node:assert/strict"; +import { Render } from "@renderinc/sdk"; -Then present the user with these verification commands to run in their own terminal: +const render = new Render(); +const result = await render.workflows.runTask("ping", []); +assert.deepEqual(result.results, ["pong"]); +console.log(result.results[0]); +``` -**Verification checklist:** +In terminal 1, from the workflow directory: -- [ ] Start the local task server: - - Python: `render workflows dev -- python workflows/main.py` - - TypeScript: `render workflows dev -- npx tsx workflows/main.ts` -- [ ] In a second terminal: `render workflows tasks list --local` -- [ ] Select `ping`, choose `run`, enter `[]` as input -- [ ] Verify the result is `"pong"` +```bash +npm run typecheck +render workflows dev -- npm run workflows:start +``` + +In terminal 2, from the same directory: + +```bash +RENDER_USE_LOCAL_DEV=true npx tsx src/client.ts +``` + +### Confirm the Result + +Each client waits for completion, checks the result, and prints `pong`. Local calls use the registered task name `ping`; deployed calls use `{workflow-slug}/ping`. The empty array supplies zero task arguments; do not pass `TaskContext`. The external client's `results` field is an array, so this task's returned string is at index `0`. + +The commands above use POSIX environment syntax. On other shells, set `RENDER_USE_LOCAL_DEV=true` in the client environment before running the script. For custom ports, set the matching `RENDER_LOCAL_DEV_URL` there too; see [local-development.md](local-development.md#trigger-local-runs-from-application-code). Keep these local settings out of deployed application environments. + +If the client fails or stays pending, inspect local runs instead of repeatedly starting new ones: + +```bash +render workflows tasks list --local -o text +render workflows tasks runs list ping --local -o text +render workflows tasks runs show --local -o json +``` -Do NOT start the dev server or run test commands yourself. Present the checklist for the user to run. +For this smoke test, bound the client wait (for example, one minute), inspect errors if it does not complete, and stop the local task server after verification when an agent started it. Run creation alone is not successful validation. -**If verification fails:** -- **Task server won't start:** check CLI version (`render --version`, needs 2.11.0+) and start command. See [Troubleshooting > Task Server Issues](troubleshooting.md#task-server-issues). -- **`ping` task not listed:** ensure the entry point imports the task file. See [Task Registration Issues](troubleshooting.md#task-registration-issues). -- **Dependency install errors:** confirm you ran install inside `workflows/`, not the project root. +For deployment, use the workflow directory as the service's root directory and preserve the build/start commands validated locally. The current CLI can generate a `render workflows create` command from `workflows init`; when scaffolding manually, construct that command from the project's actual configuration. diff --git a/skills/render-workflows/references/quick-reference.md b/skills/render-workflows/references/quick-reference.md index ea9dac1..336be3e 100644 --- a/skills/render-workflows/references/quick-reference.md +++ b/skills/render-workflows/references/quick-reference.md @@ -1,122 +1,156 @@ -# Workflows SDK Quick Reference +# Workflows SDK 1.x Quick Reference -This is an **approximate** API surface map -- not a substitute for the real SDK. -Always verify signatures against the installed package before generating code. +Use this reference for the current SDK 1.x programming model. Verify exact signatures against the installed package and the official [Python](https://render.com/docs/workflows-sdk-python) or [TypeScript](https://render.com/docs/workflows-sdk-typescript) reference before changing an existing project. -## Contents +## Version and Source Checks -- Where to find the real SDK source -- API surface map (defining tasks, task options, triggering runs, error types) -- Instance types -- Retry defaults -- Environment variables +```bash +# Python distribution and import location +python -m pip show render +python -c 'import render; print(render.__file__)' -## Where to find the real SDK source +# TypeScript package +npm ls @renderinc/sdk +``` -### Python +Current source locations: -```bash -SDK_ROOT=$(pip show render_sdk | grep Location | cut -d' ' -f2)/render_sdk - -# Signatures and source -# $SDK_ROOT/__init__.py — exports + usage examples in docstring -# $SDK_ROOT/workflows/app.py — Workflows class, @app.task, Retry, from_workflows() -# $SDK_ROOT/workflows/task.py — task internals, Options dataclass -# $SDK_ROOT/client/workflows_sync.py — sync client methods (Render().workflows.*) -# $SDK_ROOT/client/workflows.py — async client methods (RenderAsync().workflows.*) -# $SDK_ROOT/client/errors.py — error hierarchy -# $SDK_ROOT/client/types.py — TaskRun, TaskRunDetails, etc. -``` +- Python: `render/__init__.py`, `render/workflows/`, and `render/client/` +- TypeScript: `@renderinc/sdk/workflows` for task definitions and `@renderinc/sdk` for the API client +- Upstream examples: [Python](https://github.com/render-oss/sdk/tree/main/python/example) and [TypeScript](https://github.com/render-oss/sdk/tree/main/typescript/examples) -### TypeScript +## Define Tasks -```bash -TS_ROOT=node_modules/@renderinc/sdk +| Concept | Python | TypeScript | +|---|---|---| +| Imports | `from render import TaskContext, Workflows` | `import { task, type TaskContext } from "@renderinc/sdk/workflows"` | +| Register | `@app.task` | `task(options, fn)` | +| Required first parameter | `ctx: TaskContext` | `ctx: TaskContext` | +| Chain another run | `await ctx.run(task_def, *args)` | `await ctx.run(taskDef, ...args)` | +| Start task server | `app.start()` | Automatic in the workflow environment | +| Combine files | `Workflows.from_workflows(app1, app2)` | Import task modules synchronously from the entrypoint | +| In-process unit call | `task_def.func(fake_ctx, *args)` | `taskDef.func(fakeCtx, ...args)` | + +Minimal Python definition: + +```python +from render import TaskContext, Workflows + +app = Workflows() + +@app.task +def square(_ctx: TaskContext, value: int) -> int: + return value * value + +@app.task +async def sum_squares(ctx: TaskContext, left: int, right: int) -> int: + first = await ctx.run(square, left) + second = await ctx.run(square, right) + return first + second + +if __name__ == "__main__": + app.start() +``` -# Signatures and types -# $TS_ROOT/dist/workflows.d.ts — task() function, task options types -# $TS_ROOT/dist/client.d.ts — Render class, startTask, runTask, etc. -# $TS_ROOT/dist/index.d.ts — top-level exports -# $TS_ROOT/README.md — usage examples (if present) +Minimal TypeScript definition: + +```typescript +import { task, type TaskContext } from "@renderinc/sdk/workflows"; + +const square = task( + { name: "square" }, + (_ctx: TaskContext, value: number): number => value * value, +); + +task( + { name: "sumSquares" }, + async (ctx: TaskContext, left: number, right: number): Promise => { + const [first, second] = await Promise.all([ + ctx.run(square, left), + ctx.run(square, right), + ]); + return first + second; + }, +); ``` -## API surface map +## Task Options -### Defining tasks +| Purpose | Python | TypeScript | +|---|---|---| +| Custom name | `name=` | `name` | +| Retry | `retry=Retry(...)` | `retry: {...}` | +| Timeout | `timeout_seconds=` | `timeoutSeconds` | +| Compute plan | `plan=` | `plan` | -| Concept | Python | TypeScript | -|---------|--------|------------| -| Entry point | `from render_sdk import Workflows` | `import { task } from "@renderinc/sdk/workflows"` | -| Define a task | `@app.task` decorator | `task(options, fn)` | -| Start server | `app.start()` in `__main__` | Auto-starts via `RENDER_SDK_SOCKET_PATH` | -| Merge multi-file tasks | `Workflows.from_workflows(app1, app2)` | Side-effect imports in `index.ts` | -| Retry config | `Retry(max_retries, wait_duration_ms, backoff_scaling)` | `{ maxRetries, waitDurationMs, backoffScaling }` | - -### Task options - -| Option | Python (`@app.task` kwarg) | TypeScript (`task()` first arg) | -|--------|---------------------------|--------------------------------| -| Name | `name` | `name` (required) | -| Retry | `retry` (Retry instance) | `retry` (object) | -| Timeout | `timeout_seconds` | `timeoutSeconds` | -| Instance type | `plan` | `plan` | - -Workflow-level defaults: `Workflows(default_retry=..., default_timeout=..., default_plan=...)`. - -### Triggering runs (client SDK) - -| Concept | Python sync (`Render`) | Python async (`RenderAsync`) | TypeScript (`Render`) | -|---------|----------------------|----------------------------|----------------------| -| Fire-and-forget | `start_task()` | `await start_task()` | `await startTask()` | -| Start + wait | `run_task()` | `await run_task()` | `await runTask()` | -| Get run details | `get_task_run()` | `await get_task_run()` | `await getTaskRun()` | +Python workflow defaults are `default_retry`, `default_timeout`, and `default_plan` on `Workflows(...)`. + +Do not copy plan IDs, compute specifications, timeout bounds, or prices from this reference. Resolve current values from [Limits and Pricing for Render Workflows](https://render.com/docs/workflows-limits). + +Retry fields: + +| Python | TypeScript | +|---|---| +| `max_retries` | `maxRetries` | +| `wait_duration_ms` | `waitDurationMs` | +| `backoff_scaling` | `backoffScaling` | + +Retries can repeat side effects. A retried task that writes data, charges a customer, sends a message, or calls a mutating API needs an idempotency or deduplication strategy. + +## Trigger Runs with the Client + +| Operation | Python sync `Render` | Python async `RenderAsync` | TypeScript `Render` | +|---|---|---|---| +| Start without waiting | `start_task()` | `await start_task()` | `await startTask()` | +| Start and wait | `run_task()` | `await run_task()` | `await runTask()` | +| Wait on started run | Poll `get_task_run()` | `await started` | `await started.get()` | +| Get details | `get_task_run()` | `await get_task_run()` | `await getTaskRun()` | | List runs | `list_task_runs()` | `await list_task_runs()` | `await listTaskRuns()` | -| Cancel run | `cancel_task_run()` | `await cancel_task_run()` | `await cancelTaskRun()` | -| Stream events (SSE) | `task_run_events()` | `task_run_events()` | `taskRunEvents()` | +| Cancel root run | `cancel_task_run()` | `await cancel_task_run()` | `await cancelTaskRun()` | +| Stream terminal events | `task_run_events()` | `task_run_events()` async iterator | `taskRunEvents()` async iterator | -Task identifier format: `{workflow-slug}/{task-name}`. +Python input can be positional or named: -### Error types +```python +render.workflows.run_task("my-workflow/add", [2, 3]) +render.workflows.run_task("my-workflow/add", {"left": 2, "right": 3}) +``` -| Python | TypeScript | Notes | -|--------|------------|-------| -| `RenderError` | `RenderError` | Base class | -| `ClientError` | `ClientError` | 400-level | -| `RateLimitError` | — | 429 (subclass of ClientError) | -| `ServerError` | `ServerError` | 500-level | -| `TimeoutError` | — | Request timeout | -| `TaskRunError` | — | Task run failed | -| — | `AbortError` | Request aborted via AbortSignal | +TypeScript input is positional: + +```typescript +await render.workflows.runTask("my-workflow/add", [2, 3]); +``` -Import paths -- Python: `render_sdk.client.errors`, TypeScript: `@renderinc/sdk`. +Use the term **task slug** for `{workflow-slug}/{task-name}`. Task run IDs have the `trn-...` form. -### Instance types +`list_task_runs()` and `listTaskRuns()` return cursor-bearing wrapper objects; access the task run through `.task_run` in Python and `.taskRun` in TypeScript. -| Plan | Specs | -|------|-------| -| `starter` | 0.5 CPU / 512 MB | -| `standard` (default) | 1 CPU / 2 GB | -| `pro` | 2 CPU / 4 GB | -| `pro_plus` | 4 CPU / 8 GB | -| `pro_max` | 4 CPU / 16 GB | -| `pro_ultra` | 8 CPU / 32 GB | +## Error Types -`pro_plus`, `pro_max`, `pro_ultra` require requesting access. +| Python | TypeScript | Meaning | +|---|---|---| +| `RenderError` | `RenderError` | Base SDK error | +| `ClientError` | `ClientError` | Client or 4xx response | +| `RateLimitError` | `ClientError` with rate-limit status | Rate limited | +| `ServerError` | `ServerError` | Server, network, or 5xx failure | +| `TimeoutError` | — | Request timeout | +| `TaskRunError` | — | Task run failed or was canceled while waiting | +| — | `AbortError` | Client-side operation aborted | -### Retry defaults +Python error imports come from `render.client.errors`. TypeScript errors are exported from `@renderinc/sdk`. -| Scenario | `backoff_scaling` default | -|----------|--------------------------| -| Explicit retry config provided, field omitted | 1.5 | -| No retry config (built-in defaults) | 2.0 | +Aborting an SDK request or wait does not cancel the remote task run. Call the explicit cancellation method with the root task run ID. -### Environment variables +## Environment Variables | Variable | Purpose | -|----------|---------| -| `RENDER_API_KEY` | API authentication | -| `RENDER_SDK_SOCKET_PATH` | Unix socket (set by Render) | -| `RENDER_SDK_MODE` | `"run"` or `"register"` (set by Render) | -| `RENDER_SDK_AUTO_START` | TS only: `"false"` to disable | -| `RENDER_USE_LOCAL_DEV` | `"true"` for local task server | -| `RENDER_LOCAL_DEV_URL` | Custom local server URL | +|---|---| +| `RENDER_API_KEY` | Authenticate SDK calls to the Render API | +| `RENDER_SDK_SOCKET_PATH` | Internal task-runtime socket set by Render or the CLI | +| `RENDER_SDK_MODE` | Internal Python registration or run mode | +| `RENDER_SDK_AUTO_START` | Set to `false` to disable TypeScript auto-start | +| `RENDER_USE_LOCAL_DEV` | Set to `true` to use the local task server | +| `RENDER_LOCAL_DEV_URL` | Override the local task server URL | + +Do not ask users to set internal runtime variables manually. Start local workflows through `render workflows dev`. diff --git a/skills/render-workflows/references/task-patterns.md b/skills/render-workflows/references/task-patterns.md index 8775a41..2d61f01 100644 --- a/skills/render-workflows/references/task-patterns.md +++ b/skills/render-workflows/references/task-patterns.md @@ -1,57 +1,65 @@ # Task Patterns -Common workflow patterns for both Python and TypeScript. +These examples use the Render SDK 1.x task model. Every task accepts `TaskContext` first, and chained runs go through `ctx.run`. -## Contents +For Python snippets, assume: -- Fan-out / fan-in -- ETL pipeline -- Retry-heavy tasks -- Error handling in tasks -- Integration patterns (cron triggers, cross-workflow calls) +```python +from render import Retry, TaskContext, Workflows + +app = Workflows() +``` + +For TypeScript snippets, assume: + +```typescript +import { task, type TaskContext } from "@renderinc/sdk/workflows"; +``` + +## Fan-Out and Fan-In -## Fan-Out / Fan-In +Use concurrent chaining when independent work benefits from separate compute and retry boundaries. -Process a batch of items in parallel using subtasks, then aggregate results. +Python: -**Python:** ```python import asyncio @app.task -async def process_batch(image_urls: list[str]) -> dict: +def process_image(_ctx: TaskContext, url: str) -> dict: + return {"url": url, "success": True} + +@app.task +async def process_batch(ctx: TaskContext, image_urls: list[str]) -> dict: results = await asyncio.gather( - *[process_image(url) for url in image_urls] + *(ctx.run(process_image, url) for url in image_urls) ) - successful = sum(1 for r in results if r["success"]) + successful = sum(1 for result in results if result["success"]) return { "total": len(image_urls), "processed": successful, "failed": len(image_urls) - successful, "results": list(results), } - -@app.task -def process_image(url: str) -> dict: - return {"url": url, "success": True} ``` -**TypeScript:** +TypeScript: + ```typescript const processImage = task( { name: "processImage" }, - function processImage(url: string): { url: string; success: boolean } { + function processImage(_ctx: TaskContext, url: string) { return { url, success: true }; }, ); task( { name: "processBatch" }, - async function processBatch(imageUrls: string[]): Promise { + async function processBatch(ctx: TaskContext, imageUrls: string[]) { const results = await Promise.all( - imageUrls.map(url => processImage(url)), + imageUrls.map((url) => ctx.run(processImage, url)), ); - const successful = results.filter(r => r.success).length; + const successful = results.filter((result) => result.success).length; return { total: imageUrls.length, processed: successful, @@ -62,201 +70,185 @@ task( ); ``` -## ETL Pipeline +Avoid unbounded fan-out when the input can be arbitrarily large. Batch inputs or add application-level concurrency control when downstream systems, rate limits, or cost require it. -Extract, transform, load pattern with chained subtasks. +## Sequential Pipeline + +Use sequential chaining when each stage depends on the prior stage's result. + +Python: -**Python:** ```python @app.task -async def etl_pipeline(source: str, destination: str) -> dict: - raw_data = await extract(source) - transformed = await transform(raw_data) - result = await load(destination, transformed) - return result +def extract(_ctx: TaskContext, source: str) -> dict: + return {"source": source, "records": []} @app.task -def extract(source: str) -> dict: - return {"records": []} +def transform(_ctx: TaskContext, data: dict) -> dict: + return {"records": data["records"]} @app.task -def transform(data: dict) -> dict: - return {"records": []} +def load(_ctx: TaskContext, destination: str, data: dict) -> dict: + return {"destination": destination, "loaded": len(data["records"])} @app.task -def load(destination: str, data: dict) -> dict: - return {"loaded": len(data["records"])} +async def etl_pipeline( + ctx: TaskContext, + source: str, + destination: str, +) -> dict: + raw_data = await ctx.run(extract, source) + transformed = await ctx.run(transform, raw_data) + return await ctx.run(load, destination, transformed) ``` -**TypeScript:** +TypeScript: + ```typescript const extract = task( { name: "extract" }, - function extract(source: string): object { - return { records: [] }; - }, + (_ctx: TaskContext, source: string) => ({ source, records: [] as object[] }), ); const transform = task( { name: "transform" }, - function transform(data: object): object { - return { records: [] }; - }, + (_ctx: TaskContext, data: { records: object[] }) => ({ records: data.records }), ); const load = task( { name: "load" }, - function load(destination: string, data: object): object { - return { loaded: 0 }; - }, + (_ctx: TaskContext, destination: string, data: { records: object[] }) => ({ + destination, + loaded: data.records.length, + }), ); task( { name: "etlPipeline" }, - async function etlPipeline(source: string, destination: string): Promise { - const rawData = await extract(source); - const transformed = await transform(rawData); - return await load(destination, transformed); + async function etlPipeline( + ctx: TaskContext, + source: string, + destination: string, + ) { + const rawData = await ctx.run(extract, source); + const transformed = await ctx.run(transform, rawData); + return ctx.run(load, destination, transformed); }, ); ``` -## Retry-Heavy Tasks +## Retries and Idempotency -Tasks with aggressive retry for unreliable external APIs. +Tasks retry automatically by default. Before running tasks that send emails, charge payments, or modify external state, use idempotency keys or another deduplication mechanism to prevent duplicate effects. -**Python:** -```python -from render_sdk import Retry +Python: -@app.task(retry=Retry(max_retries=5, wait_duration_ms=2000, backoff_scaling=2.0)) -def call_external_api(endpoint: str, payload: dict) -> dict: +```python +@app.task( + retry=Retry( + max_retries=5, + wait_duration_ms=2000, + backoff_scaling=2.0, + ) +) +def fetch_document(_ctx: TaskContext, url: str) -> str: import urllib.request - import json - req = urllib.request.Request( - endpoint, - data=json.dumps(payload).encode(), - headers={"Content-Type": "application/json"}, - ) - with urllib.request.urlopen(req, timeout=30) as resp: - return json.loads(resp.read()) + with urllib.request.urlopen(url, timeout=30) as response: + return response.read().decode() ``` -**TypeScript:** +TypeScript: + ```typescript task( { - name: "callExternalApi", - retry: { maxRetries: 5, waitDurationMs: 2000, backoffScaling: 2.0 }, - timeoutSeconds: 60, + name: "fetchDocument", + retry: { + maxRetries: 5, + waitDurationMs: 2000, + backoffScaling: 2, + }, }, - async function callExternalApi(endpoint: string, payload: object): Promise { - const resp = await fetch(endpoint, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - if (!resp.ok) throw new Error(`HTTP ${resp.status}`); - return resp.json(); + async function fetchDocument(_ctx: TaskContext, url: string): Promise { + const response = await fetch(url); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.text(); }, ); ``` -## Error Handling in Tasks +Do not retry every error indiscriminately in task code. Let the configured task policy handle run retries, and distinguish permanent validation failures from transient upstream failures when the task itself implements request-level retry logic. + +## Testing Task Logic In Process -Tasks that raise exceptions are retried automatically according to their retry config. Use this intentionally. For common execution issues and fixes, see [troubleshooting.md](troubleshooting.md). +SDK 1.x returns task definitions rather than callable task wrappers. Invoke `.func` with a test context for unit tests: + +Python: -**Python:** ```python -@app.task(retry=Retry(max_retries=3, wait_duration_ms=1000)) -def resilient_task(data: dict) -> dict: - result = process(data) - if not result["valid"]: - raise ValueError("Invalid result, retrying...") - return result +class NoSubtasks: + async def run(self, task, *args, **kwargs): + raise AssertionError("unexpected chained run") + +assert process_image.func(NoSubtasks(), "https://example.com/a.png")["success"] ``` -**TypeScript:** +TypeScript: + ```typescript -task( - { - name: "resilientTask", - retry: { maxRetries: 3, waitDurationMs: 1000 }, - }, - function resilientTask(data: object): object { - const result = process(data); - if (!result.valid) { - throw new Error("Invalid result, retrying..."); - } - return result; +const noSubtasks: TaskContext = { + run: async () => { + throw new Error("unexpected chained run"); }, -); -``` +}; ---- +const result = processImage.func(noSubtasks, "https://example.com/a.png"); +``` -## Integration Patterns +For orchestration tests, supply a fake `TaskContext.run` that records task definitions and returns controlled results. -Patterns for triggering workflow tasks from outside a workflow context. +## Scheduled Trigger -## Cron-Triggered Workflows +Render Workflows does not currently provide native scheduling. A Render cron job can trigger a deployed task with the SDK client. -Render Workflows do not have built-in scheduling. Use a Render cron job to trigger tasks on a schedule. +Python sync client: -**Python cron job (synchronous)** (`cron_trigger.py`): ```python -from render_sdk import Render +from render import Render -def main(): +def main() -> None: render = Render() result = render.workflows.run_task("my-workflow/daily-cleanup", []) - print(f"Cleanup completed: {result.status}") + print(result.status) if __name__ == "__main__": main() ``` -**Python cron job (asynchronous)** (`cron_trigger.py`): -```python -import asyncio -from render_sdk import RenderAsync - -async def main(): - render = RenderAsync() - started = await render.workflows.start_task("my-workflow/daily-cleanup", []) - finished = await started - print(f"Cleanup completed: {finished.status}") +TypeScript: -asyncio.run(main()) -``` - -**TypeScript cron job** (`cron-trigger.ts`): ```typescript import { Render } from "@renderinc/sdk"; const render = new Render(); - -async function main() { - const result = await render.workflows.runTask("my-workflow/daily-cleanup", []); - console.log("Cleanup completed:", result.status); -} - -main(); +const result = await render.workflows.runTask("my-workflow/daily-cleanup", []); +console.log(result.status); ``` -Set up the cron job in the Render Dashboard or via MCP with the desired schedule. +Store `RENDER_API_KEY` as a secret on the triggering service. -## Cross-Workflow Calls +## Cross-Workflow Call -A task in one workflow can trigger tasks in a **different** workflow using the SDK client (not subtask syntax): +`ctx.run` only chains tasks in the same workflow service. To call a different workflow, use the API client. This creates an independent run rather than a parent-child relationship in the workflow graph. + +Python: -**Python:** ```python -from render_sdk import RenderAsync +from render import RenderAsync @app.task -async def orchestrate(data: dict) -> dict: +async def orchestrate(_ctx: TaskContext, data: dict) -> object: render = RenderAsync() result = await render.workflows.run_task( "other-workflow/process", @@ -265,13 +257,14 @@ async def orchestrate(data: dict) -> dict: return result.results ``` -**TypeScript:** +TypeScript: + ```typescript import { Render } from "@renderinc/sdk"; task( { name: "orchestrate" }, - async function orchestrate(data: object): Promise { + async function orchestrate(_ctx: TaskContext, data: object): Promise { const render = new Render(); const result = await render.workflows.runTask( "other-workflow/process", @@ -282,4 +275,4 @@ task( ); ``` -> **Note:** Cross-workflow calls are not tracked as subtask relationships in the Dashboard. +Cross-workflow calls require an API key and are subject to Render API behavior. Prefer same-workflow chaining when the tasks form one logical execution graph. diff --git a/skills/render-workflows/references/troubleshooting.md b/skills/render-workflows/references/troubleshooting.md index 62b20c7..278b896 100644 --- a/skills/render-workflows/references/troubleshooting.md +++ b/skills/render-workflows/references/troubleshooting.md @@ -1,234 +1,213 @@ # Troubleshooting -Common issues and fixes when developing with Render Workflows, sourced from the SDK source code and official docs. +Use this reference for common Render Workflows setup, registration, execution, and client failures. Check the current [Workflows documentation](https://render.com/docs/workflows) and installed SDK version before relying on an implementation detail. -## Contents +## CLI and Local Server -- Task server issues -- Task registration issues -- Task execution issues -- Client issues -- Connection and networking -- Local development gotchas -- Limits reference +### `render workflows` or a subcommand is unavailable -## Task Server Issues +Run `render --version` and upgrade the CLI. Different workflow features arrived in different versions; this skill uses 2.16.0 or later for scaffolding, local development, and CLI deployment. -### Task server won't start +On macOS with Homebrew: -**Symptom:** `render workflows dev` fails or hangs. +```bash +brew upgrade render +``` -| Cause | Fix | -|-------|-----| -| CLI version < 2.11.0 | Run `render --version` to check. Upgrade: `brew upgrade render` (macOS) or reinstall via the install script. | -| Wrong start command | Python: `render workflows dev -- python workflows/main.py`. TypeScript: `render workflows dev -- npx tsx workflows/main.ts`. | -| Missing `app.start()` (Python) | Add `if __name__ == "__main__": app.start()` at the bottom of your entry point. | -| Port already in use | Use `--port` flag: `render workflows dev --port 8121 -- ...` | +For other platforms, follow the current [CLI installation guide](https://render.com/docs/cli#installation). -### `app.start()` fails with `ValueError` (Python) +### Local task server does not start -**Symptom:** `ValueError` about missing `RENDER_SDK_MODE` or `RENDER_SDK_SOCKET_PATH`. +| Cause | Resolution | +|---|---| +| Start command is wrong | Run the same entrypoint used by the workflow service, such as `python main.py` or `npm start`. | +| Python dependencies are in a virtual environment | Use its interpreter, such as `.venv/bin/python main.py`. | +| Python entrypoint does not call `app.start()` | Add the call to the executed entrypoint. | +| Port is occupied | Start with `--port ` and pass the same port to every local CLI or SDK call. | +| Docker-based workflow | Use a native local entrypoint or test after deployment; Docker workflows do not currently support the local task server. | -**Cause:** `app.start()` expects env vars that the Render CLI sets automatically. Running `python main.py` directly (without the CLI) triggers this. +Do not run a Python workflow entrypoint directly when it expects Render runtime variables. Start it through: -**Fix:** Always start via the CLI: `render workflows dev -- python workflows/main.py`. The CLI sets the required env vars. +```bash +render workflows dev -- python main.py +``` -### Auto-start crashes with `process.exit(1)` (TypeScript) +### Environment variables are missing locally -**Symptom:** Process exits immediately with no useful error message. +The CLI loads `.env` from its current directory. Run from the intended workflow root, or pass one or more explicit files: -**Cause:** The SDK auto-starts the task server via `setImmediate` when `RENDER_SDK_SOCKET_PATH` is set. If `startTaskServer()` throws, the SDK calls `process.exit(1)` with only a `console.error`. +```bash +render workflows dev --env-file .env --env-file .env.local -- python main.py +``` -**Fix:** Check that `RENDER_SDK_SOCKET_PATH` points to a valid, writable path. Use the Render CLI for local dev. Set `RENDER_SDK_AUTO_START=false` to disable auto-start and start manually if needed. +Later files override earlier ones. Confirm that secrets are ignored by Git and do not print their values during diagnosis. -## Task Registration Issues +## SDK 1.x Migration Failures -### "Task not found" in CLI +### Python cannot import `render_sdk` -**Symptom:** `render workflows tasks list --local` shows no tasks or is missing expected tasks. +The current Python distribution and import module are named `render`: + +```bash +python -m pip install 'render>=1.0.1' +python -c 'import render; print(render.__file__)' +``` -| Cause | Fix | -|-------|-----| -| Module not imported (Python) | Ensure your entry point imports all task files or uses `Workflows.from_workflows()`. | -| Module not imported (TypeScript) | Add `import './your-task-file'` to your `index.ts` entry point. | -| Server not running | Start the local task server first, then run `list --local` in a second terminal. | -| Missing `--local` flag | Without `--local`, the CLI lists deployed (remote) tasks, not local ones. | +Update imports such as: -### Late registration after auto-start (TypeScript) +```python +from render import Render, Retry, TaskContext, Workflows +``` -**Symptom:** Dynamically imported tasks are not available for execution; a warning appears in logs. +### Task registration rejects the function signature -**Cause:** The SDK auto-starts via `setImmediate` after synchronous module loading. Tasks registered after that (e.g., via dynamic `import()`) miss the registration window. +Every task must accept `TaskContext` as its first positional parameter, including tasks that do not use it: -**Fix:** Define all tasks at module scope using synchronous `import './module'` statements. Avoid dynamic imports for task files. +```python +@app.task +def ping(_ctx: TaskContext) -> str: + return "pong" +``` -### Duplicate task names +```typescript +task({ name: "ping" }, (_ctx: TaskContext) => "pong"); +``` -**Symptom:** Python raises `ValueError("Task '{name}' already registered")`. TypeScript silently overwrites the previous task. +The context is supplied by Render and is not included in CLI, SDK, or API task input. -**Cause:** Two tasks registered with the same `name`. +### A registered task is “not callable” -**Fix:** Use unique names for every task. When using `Workflows.from_workflows()` in Python, a `ValueError` is raised if any imported apps share a task name. +SDK 1.x returns a task definition, not a callable wrapper. -### Empty task name (TypeScript) +- From another task, use `await ctx.run(task_definition, ...args)`. +- In a unit test, call `task_definition.func(fake_context, ...args)` explicitly. +- From an application or script, use `Render().workflows.start_task()` or `run_task()` and the task slug. -**Symptom:** `Error: Task function must have a name or name must be provided`. +Do not restore direct calls as a workaround; direct calls would bypass distributed execution and task-run observability. -**Fix:** Always pass `name` in the options object: `task({ name: "myTask" }, function myTask() { ... })`. +## Task Registration -## Task Execution Issues +### Local task list is empty or missing tasks -### Subtask hangs forever +| Cause | Resolution | +|---|---| +| Missing `--local` | Use `render workflows tasks list --local`. | +| Server is not running | Start `render workflows dev` in another terminal first. | +| Python module is not incorporated | Import its `Workflows` object and combine apps with `Workflows.from_workflows(...)`. | +| TypeScript module is not imported | Add a synchronous module import to the entrypoint. | +| TypeScript task registered after auto-start | Define and import tasks synchronously at module scope; avoid dynamic imports for registration. | +| Duplicate task name | Give every task a unique registered name. | -**Symptom:** A task that calls other tasks never completes. +TypeScript's registry can replace a previous task with the same name, so detect duplicates during review rather than relying on a runtime error. -| Cause | Fix | -|-------|-----| -| Missing `await` | Subtask calls return a `TaskInstance` (Python) or `Promise` (TypeScript), not the result. You must `await` them. | -| Missing `async` keyword | Python: chaining tasks must be declared `async def`. TypeScript: must use `async function`. | -| Sequential instead of parallel | If you `await` each subtask individually, they run serially. Use `asyncio.gather()` (Python) or `Promise.all()` (TypeScript) for parallel execution. | +### TypeScript exits during auto-start -### Calling subtask outside a task context (Python) +The TypeScript SDK auto-starts task registration when `RENDER_SDK_SOCKET_PATH` is present. Use the Render CLI locally. Only set `RENDER_SDK_AUTO_START=false` when deliberately taking manual control of startup. -**Symptom:** `RuntimeError` about running a subtask outside task execution context. +## Task Execution -**Cause:** Task functions that are decorated with `@app.task` can only trigger subtask runs when called from within another executing task. Calling them from regular code (e.g., a script or REPL) fails because the internal `_current_client` context is not set. +### Chained run hangs or never starts -**Fix:** To trigger tasks from outside a task, use the SDK client (`Render()` or `RenderAsync()`) with `start_task()` or `run_task()`. Subtask syntax is only for task-to-task chaining. +Check that: -### Mixed positional and keyword arguments (Python) +- The parent task is `async`. +- The child is registered in the same workflow service. +- The call is `await ctx.run(child, ...)`. +- Parallel calls are collected with `asyncio.gather`, `asyncio.TaskGroup`, `Promise.all`, or a deliberate equivalent. -**Symptom:** `ValueError` about not mixing positional and keyword arguments. +Awaiting each independent child one at a time makes the chain serial rather than parallel. -**Cause:** The Python SDK does not allow calling a subtask with both `*args` and `**kwargs` simultaneously. +### Python rejects mixed task arguments -**Fix:** Use either positional args or keyword args, not both: `await my_task(1, 2)` or `await my_task(a=1, b=2)`. +`ctx.run` accepts positional arguments or named arguments, not both in the same call: -### "Not JSON serializable" error +```python +await ctx.run(task, first, second) +await ctx.run(task, left=first, right=second) +``` -**Symptom:** Task fails with a serialization error. +The same distinction applies when triggering a Python task through the SDK: send a list for positional input or a dictionary for named input. -| Cause | Fix | -|-------|-----| -| Non-serializable arguments | Task args must be JSON-serializable: dicts/objects, lists/arrays, strings, numbers, booleans, None/null. No class instances, functions, dates, sets, bytes, or `BigInt`. | -| Non-serializable return value | Same rule applies to return values. Convert complex objects to dicts/plain objects before returning. | -| Non-serializable default values (Python) | Default parameter values that aren't JSON-serializable are silently dropped. The API metadata won't reflect them. Use only JSON-serializable defaults. | +### Input or result is not JSON-serializable -### Task run fails silently +Convert values to JSON-compatible objects before passing or returning them. In particular, encode dates, byte strings, sets, class instances, and TypeScript `BigInt` values explicitly. -**Symptom:** Task run shows `failed` status with no useful error. +For current payload-size limits, consult [Limits and Pricing for Render Workflows](https://render.com/docs/workflows-limits). -| Cause | Fix | -|-------|-----| -| Unhandled exception | Add logging inside your task. Wrap risky code in try/except (Python) or try/catch (TypeScript). | -| No retry config | Add retry configuration so transient failures are retried automatically. | -| Timeout exceeded | Default timeout is 2 hours. Set `timeout_seconds` (Python) or `timeoutSeconds` (TypeScript) per task if you need more. Max: 24 hours. | -| All retries exhausted | After `max_retries + 1` total attempts, the run is marked failed. Inspect `TaskRunDetails.attempts` for per-attempt error details. | +### Retried task duplicates a side effect -### Timeout value rejected by API +Retries rerun task logic. Use idempotency keys, upserts, transactional guards, or downstream deduplication for writes, payments, messages, and other mutations. Do not enable retries on unsafe operations without a repeat-safety strategy. -**Symptom:** Task registration or run fails with a validation error. +### Timeout or compute plan is rejected -**Cause:** The SDK does not validate `timeout_seconds`/`timeoutSeconds` client-side. Out-of-range values (outside 30–86,400) are sent to the API, which rejects them. +Verify the task option name first: -**Fix:** Keep timeout values between 30 seconds and 86,400 seconds (24 hours). +- Python task: `timeout_seconds=` and `plan=` +- Python workflow default: `default_timeout=` and `default_plan=` +- TypeScript task: `timeoutSeconds` and `plan` -## Client Issues +Then resolve current timeout bounds and compute-plan IDs from [Limits and Pricing for Render Workflows](https://render.com/docs/workflows-limits). Do not substitute a remembered legacy plan name. -### `Render()` with `await` fails (Python) +## API Client -**Symptom:** `TypeError` or unexpected behavior when using `await` with `Render()`. +### `await Render()` fails in Python -**Fix:** `Render()` is the synchronous client. Use `RenderAsync()` for async contexts: +`Render` is synchronous. Use `RenderAsync` in an async context: ```python -# Synchronous (Flask, Django, scripts) -from render_sdk import Render -render = Render() -result = render.workflows.run_task("my-workflow/task", [42]) +from render import RenderAsync -# Asynchronous (FastAPI, async scripts) -from render_sdk import RenderAsync render = RenderAsync() result = await render.workflows.run_task("my-workflow/task", [42]) ``` -### "Invalid API key" or "Unauthorized" - -| Cause | Fix | -|-------|-----| -| Missing `RENDER_API_KEY` | Set the environment variable: `export RENDER_API_KEY=rnd_...` | -| Wrong key | Generate a new key at `https://dashboard.render.com/u/*/settings#api-keys` | -| Key doesn't match workspace | Ensure the key belongs to the workspace that owns the workflow | -| No key and no token passed | Both `Render()` and `RenderAsync()` raise `ValueError` if no token is available | - -### Argument too large - -**Symptom:** Request rejected with a size error. +Use `Render` without `await` in synchronous code. -**Fix:** Task arguments cannot exceed 4 MB total per invocation. Reduce payload size or pass references (URLs, IDs) instead of raw data. +### Unauthorized or API key missing -### SSE stream ends without event +The SDK uses `RENDER_API_KEY` unless a token is passed to the constructor. Confirm that the key belongs to a user with access to the workflow's workspace. Never log or commit the key. -**Symptom:** `RenderError("Task run completed with no event")` (Python) or unhandled stream error (TypeScript). +### Client wait was aborted but the run continues -**Cause:** The SSE connection closed before a terminal event (`completed`, `failed`, `canceled`) was received. +Canceling a TypeScript `AbortSignal`, closing an SSE stream, or interrupting a local wait does not cancel the remote run. Call the explicit cancellation method with the root task run ID: -**Fix:** This is typically a transient network issue. The SDK retries internally (up to 5 attempts with exponential backoff). If it persists, check network connectivity and try again. You can also poll with `get_task_run()` as a fallback. - -### AbortSignal does not cancel remote task (TypeScript) - -**Symptom:** After aborting, the task run continues executing on Render. - -**Cause:** `AbortSignal` only cancels the local SDK wait (the HTTP request or SSE stream). The remote task run keeps running. +```typescript +await render.workflows.cancelTaskRun(taskRunId); +``` -**Fix:** To actually cancel a running task, explicitly call `render.workflows.cancelTaskRun(taskRunId)` after aborting. +Canceling a root run cancels its active chained runs. A child run is not an independent cancellation target. -### Rate limiting (429) +### Run listing shape is unexpected -**Symptom:** `RateLimitError` (Python) or `ClientError` with 429 status (TypeScript). +Current list methods return cursor-bearing wrappers: -**Fix:** Reduce request frequency. The SDK retries rate-limited requests internally for UDS calls (up to 15 attempts), but client-to-API rate limits require you to back off. +- Python: access `.task_run` on each item. +- TypeScript: access `.taskRun` on each item. -## Connection & Networking +Use the returned cursor for pagination instead of assuming one call returns all runs. -### UDS retries exhausted +### Rate limiting or queued runs -**Symptom:** Error after approximately 2.5 minutes of retries during task execution. +API-triggered runs and chained runs have different limits and queueing behavior. Consult [Limits and Pricing for Render Workflows](https://render.com/docs/workflows-limits) for current API rate, compute, and queueing rules. Back off on rate-limit responses; do not assume those requests were queued. -**Cause:** The internal Unix Domain Socket client retries transient errors (5xx, timeouts, rate limits) up to 15 times with exponential backoff (250ms initial, 2x factor, 16s cap). After 15 failures, the last error is thrown. +## Deployment -**Fix:** This usually indicates the task server is unhealthy or overwhelmed. Check server logs, restart the task server, and ensure the socket path is valid. +### `render workflows create` fails -### Runs queued at concurrency limit +Confirm the active workspace, backing Git remote, repository access, runtime, root directory, and exact build/run commands. For non-interactive creation, required fields must be passed as flags. -**Symptom:** New task runs stay in `pending` status for a long time. +If `--repo .` cannot resolve the repository, inspect the local Git remote and pass the supported Git provider URL explicitly. -**Cause:** Your workspace has hit its concurrent run limit. New runs are queued (not rejected) until another run completes. +### Tasks do not appear after deployment -**Fix:** Wait for in-progress runs to finish, cancel unnecessary runs, or purchase additional concurrency in your workspace settings. Creating multiple workflow services does not increase the limit. +Check the workflow's build and release logs. Confirm that the deployed start command executes the same entrypoint verified locally and that all task modules are imported during registration. -## Local Development Gotchas +To release a new version explicitly and wait for the outcome: -| Gotcha | Details | -|--------|---------| -| Local IDs don't match production | Task and run IDs in local dev are random UUIDs. They won't match deployed identifiers. | -| Data is in-memory only | Logs and results are lost when the local server shuts down. | -| Memory grows with many runs | Restart the local server periodically during heavy testing. | -| Only task endpoints are simulated | Other Render API endpoints are not available on the local server. | -| Cross-workflow calls not tracked | Calls between workflows via the SDK client are not shown as chained runs in the Dashboard. | +```bash +render workflows versions release --wait +``` -## Limits Reference +### Blueprint behavior is unclear -| Limit | Value | -|-------|-------| -| Max task definitions per workflow | 500 | -| Max argument size per run | 4 MB | -| Concurrent runs (Hobby plan) | 20 base, up to 200 | -| Concurrent runs (Pro plan) | 50 base, up to 200 | -| Concurrent runs (Scale plan) | 100 base, up to 300 | -| Concurrent runs (Enterprise plan) | 100 base, up to 300+ | -| Run timeout range | 30 seconds – 24 hours | -| Default run timeout | 2 hours | -| UDS internal retries | 15 attempts over ~2.5 minutes | -| SSE wait retries | 5 attempts with exponential backoff | +Blueprint support for Workflows is evolving. Check the current [Workflows FAQ](https://render.com/docs/workflows#faq), [Blueprint specification](https://render.com/docs/blueprint-spec), CLI version, and public changelog before generating or modifying `render.yaml` for a workflow. Do not rely on a cached claim of support or non-support.