Render Tutorials
Deploy an AI agent three ways on Render

What you'll build

⏱ 5 min

Overview

In this tutorial you deploy a small AI code-review agent on Render three times — each time as a full stack via a Blueprint. You deploy the same agent three ways — as an in-process web service, as a queue-backed worker, and as Render Workflows — and watch the runtime pattern absorb more of the coordination each time. All three stacks stay up so you can compare them side by side. The agent code is identical in all of them. The only thing that changes is where the work runs.

The agent

The agent reviews a public GitHub pull request. It runs a fixed pipeline:

flowchart LR
  pr(["GitHub PR URL"])
  prep["prepare diff<br/>fetch per-file patches"]
  filter["filter diff<br/>drop lock files + bundles"]
  sec["security"]
  perf["performance"]
  ux["ux (frontend only)"]
  judge["judge<br/>approve / request-changes"]

  pr --> prep --> filter
  filter --> sec
  filter --> perf
  filter --> ux
  sec --> judge
  perf --> judge
  ux --> judge
  • The prepare-diff step fetches the changed files from a public PR through the GitHub API.
  • The filter-diff step drops noise such as lock files, minified bundles, and source maps before any tokens are spent. It returns the kept patches plus the list of dropped files, so the decision is visible in telemetry.
  • security and performance always run. ux joins only when the diff touches frontend files. judge consolidates the findings into an approve or request-changes verdict.

The whole agent lives once in shared/agent and is imported unchanged by every pattern.

Repo layout

The repo is a monorepo. Three packages each implement one runtime pattern, and three shared modules supply the code every pattern reuses.

workflow-agents-workshop-ts/
├── packages/
│ ├── naive-agent/
│ │ └── server.ts # HTTP handler that awaits the review in-process
│ ├── queue-agents/
│ │ ├── web.ts # Web tier (producer) — enqueues jobs, streams progress via SSE
│ │ ├── worker.ts # Background worker (consumer) — pulls jobs off the Valkey stream
│ │ └── kv.ts # Valkey stream + pub/sub helpers (XADD, XREADGROUP, XACK)
│ └── workflow-agents/
│ ├── server.ts # Gateway — turns PR submissions into Render Workflow runs
│ └── workflow.ts # Workflow service — auto-discovers and registers task definitions
├── shared/
│ ├── agent/ # The review pipeline, imported unchanged by every pattern
│ │ ├── review.ts # Re-exports and types for the pipeline composition
│ │ ├── agents.ts # Agent definitions (security, performance, ux, judge)
│ │ ├── prepareDiff.ts # Fetches per-file patches from the GitHub API
│ │ ├── model.ts # LLM provider selection (or mock model when no key is set)
│ │ └── loop.ts # Agent execution loop
│ ├── db/ # Telemetry store — reviews, findings, and agent spans
│ │ ├── index.ts # Auto-selects Postgres (DATABASE_URL) or in-memory backend
│ │ └── memory.ts # In-memory backend for zero-setup local dev
│ └── ui/ # Dashboard served by every pattern
│ └── page.ts # Renders the telemetry viewer HTML shell
├── package.json # npm workspaces root
└── tsconfig.base.json

The repo uses npm workspaces. Each package and shared module is a workspace with its own package.json. A single npm install at the root resolves every dependency, and packages import shared modules as workspace references.

workflow-agents-workshop-py/
├── packages/
│ ├── naive_agent/
│ │ └── server.py # HTTP handler that awaits the review in-process
│ ├── queue_agents/
│ │ ├── web.py # Web tier (producer) — enqueues jobs, streams progress via SSE
│ │ ├── worker.py # Background worker (consumer) — pulls jobs off the Valkey stream
│ │ └── kv.py # Valkey stream + pub/sub helpers (XADD, XREADGROUP, XACK)
│ └── workflow_agents/
│ ├── server.py # Gateway — turns PR submissions into Render Workflow runs
│ └── workflow.py # Workflow service — auto-discovers and registers task definitions
├── shared/
│ ├── agent/ # The review pipeline, imported unchanged by every pattern
│ │ ├── review.py # Re-exports and types for the pipeline composition
│ │ ├── agents.py # Agent definitions (security, performance, ux, judge)
│ │ ├── prepare_diff.py # Fetches per-file patches from the GitHub API
│ │ ├── model.py # LLM provider selection (or mock model when no key is set)
│ │ └── loop.py # Agent execution loop
│ ├── db/ # Telemetry store — reviews, findings, and agent spans
│ │ ├── __init__.py # Auto-selects Postgres (DATABASE_URL) or in-memory backend
│ │ └── memory.py # In-memory backend for zero-setup local dev
│ └── ui/ # Dashboard served by every pattern
│ └── __init__.py # Mountable FastAPI router for the telemetry viewer
├── pyproject.toml # uv workspace root
└── uv.lock

The repo uses a uv workspace. Each package and shared module has its own pyproject.toml. A single uv sync at the root resolves every dependency, and packages import shared modules as workspace path dependencies.

You only touch files inside one package at a time. The shared/ modules stay as-is throughout the tutorial.

One agent, three runtime patterns

This tutorial runs the agent three ways:

PatternRuntime patternRender resources
Naive agentOne web service, in-processWeb Service + Postgres
Queue agentsWeb + background worker + queueWeb Service + Background Worker + Key Value + Postgres
Workflow agentsRender WorkflowsWeb Service + Workflows + Postgres

The point of the exercise is the contrast. The same review, run on a richer runtime pattern, gets durability and scale you would otherwise build by hand.

Three patterns, one fan-out

PatternHow fan-out is implementedWhat you maintain
1. NaiveParallel calls in one processNothing, but no scale or durability
2. QueueEnqueue, consumer group, acks, pub/subThe whole queue
3. WorkflowParallel reviewer tasksNothing. Render does it

Pattern 2’s queue helper — the stream, consumer group, acks, retries, and pub/sub — is, in Pattern 3, the Render platform. The agent never changes.

Why deploy-first

You could run all of this locally. This tutorial deploys instead, because the lesson is about runtime behavior on Render. A queue that survives a crash, a worker you scale to three instances, a review that finishes after you redeploy the web tier: none of that shows up on a single laptop process. Local commands stay in this tutorial as a quick fallback, not the main path.

Demo PRs

The reviewer works against any public GitHub PR. The tutorial uses a small set of public demo PRs so everyone can run the same flow without creating test repos or GitHub webhooks.

Each deployed service serves the same dashboard. You paste or pick a PR URL, click Review, and compare the Status, Workflow, token count, run time, findings, and spans across runtime patterns.

Before you start

Set these up first:

  • A Render account. All services in this tutorial use paid plans (Starter for compute, basic-256mb for Postgres). Delete them when you’re done to avoid charges.

  • A GitHub account. Fork the repo. You’ll deploy from your own copy of the repo.

  • The Render CLI installed:

    Terminal window
    # Install the CLI (macOS / Linux)
    curl -fsSL https://raw.githubusercontent.com/render-oss/cli/refs/heads/main/bin/install.sh | sh
    # Log in
    render login
  • Clone the repo so you have it locally for later steps:

Terminal window
git clone https://github.com/<your-github-username>/workflow-agents-workshop-ts.git
cd workflow-agents-workshop-ts
npm ci
Terminal window
git clone https://github.com/<your-github-username>/workflow-agents-workshop-py.git
cd workflow-agents-workshop-py
uv sync

By the end you’ll have three runtime patterns deployed and a clear sense of which one you’d reach for.

Across the two patterns you deploy here, what actually changes?
Troubleshooting

Find the symptom that matches what you’re seeing, then apply the fix.

Will I be charged? Every service runs on a paid plan. Budget a few dollars if you leave services running, and delete them when you’re done.


Reviews fail with a GitHub 403 and no findings. GitHub’s API allows only 60 unauthenticated requests per hour, per IP. A shared network exhausts that fast, and prepare-diff then returns 403 rate limit exceeded. Create a GitHub token (a fine-grained PAT with public-repo read is enough) and set it as GITHUB_TOKEN on each service. That raises the limit to 5,000/hour. The GITHUB_TOKEN slot already exists in every Blueprint as a sync: false env var.


render workflows ... says unknown command. Your CLI is older than 2.11. Homebrew can lag; reinstall via the releases page or the curl script, then confirm render --version.


The review output looks canned and always approves. That’s the deterministic mock model, the expected fallback when no provider key is set. It proves the deploy, request, and telemetry path end to end. Set ANTHROPIC_API_KEY or OPENAI_API_KEY (on the service that runs the review) for real findings.


Local fallbacks or tests fail on an older Node. This repo needs Node.js 22.12 or newer (node --version). On Node 20, npm install errors with Unsupported engine.


Local fallbacks fail with command not found: uv or ModuleNotFoundError. The Python track needs Python 3.12+ and uv. Run every local command through uv run (for example uv run python -m naive_agent.server), because the workspace packages live in the uv-managed .venv.

What you learned

  • One code-review agent runs the pipeline prepare diff -> filter diff -> [security, performance, ux?] -> judge
  • You deploy it three ways: a naive web service, a worker plus queue, and as Render Workflows
  • Pattern 3 (Workflows) replaces the queue coordination — the platform handles fan-out, retries, and delivery
  • The filter-diff step drops noise before the expensive fan-out and records what it dropped
  • Forked the workshop repo and namespaced Blueprint resources with your GitHub username
  • You can review the same public PR against each pattern so the runtime pattern is the only variable
  • Without an LLM key the agent uses a mock model that returns canned output, not real findings