The teaching layer on top of the learner-facing tutorials. The tutorials carry the deploy steps, repo layout, and code context; this guide carries the talk tracks, timing, aha moments, hint ladders, and worked solutions.
Read once end-to-end before your first run. Keep the Run sheet and Solutions open on a second screen while you present.
SAME AGENT ───────────────────────────────────▶ (never changes)
Pattern 1 [ web request runs the agent ] you own: nothing
└ breaks: timeouts, lost on deploy, no scale scales: no
Pattern 2 [ web ] → (Valkey queue) → [ worker runs agent ] you own: queue,
└ durable, scales by adding workers consumer group,
acks, retries, pub/sub
Pattern 3 [ web ] → Render Workflows → [ task per agent ] you own: nothing
└ same durability + scale, declarative scales: yes
The emotional arc you're selling:
- Pattern 1 feels good ("look, it's live") → then you break it.
- Pattern 2 is powerful ("now it's durable and scales!") → then they read the ack contract and see how much coordination they now own.
- Pattern 3 feels like cheating ("wait, that's it?") → the guarantees from
Pattern 2 collapse into
retry: { maxRetries: 2 }, a CLI-created Workflow, and a trace.
If learners leave able to recite "the agent never changed, the substrate did the work," the workshop succeeded.
- Total time: ~1h 50 mins, designed as two sessions with a 10 minute break.
- Session 1 — Substrates & coordination (~50 min): Patterns 1 & 2, including tracing the ack contract and scaling the worker.
- Session 2 — Let the platform (and agents) do it (~50 min): Pattern 3 and the author-a-task finale, where coding agents come out.
- Format: live deploys + a hands-on lab. Learners follow along on their own Render accounts and machines.
- Group size: works 1:1 up to ~30 with a helper for debugging environments.
- Delivery: in-person or remote. Remote works fine. Have learners share service URLs, CLI output, and Dashboard screenshots when they get stuck.
Do this before learners arrive (and have learners do the install ahead of time if you can — environment setup is the #1 time sink).
Facilitator machine:
- Node >= 22.12 (
node -v). -
npm installfrom the repo root completes clean. - Render CLI installed, logged in, and pointed at the right workspace
(
render login, thenrender workspace set). - A fork or workshop repo connected to Render.
- Pattern 1 and Pattern 2 Blueprints tested from that repo.
- Pattern 3 hybrid path rehearsed with the web+Postgres Blueprint and
render workflows create. - Optional local Valkey running (
valkey-server &ordocker run -p 6379:6379 valkey/valkey). -
npm testis green (proves the mock model path works end-to-end). - Decide: real model or mock? With no LLM provider API key everything runs
on a deterministic mock model — totally fine and fully offline. Set
ANTHROPIC_API_KEY/OPENAI_API_KEYonly if you want live reviews. HaveAGENT_MODEL=mockready as a fallback if the gateway misbehaves on stage.
Room / screen:
- Terminal font large enough to read from the back.
- Browser tabs open to the Render Dashboard, the learner-facing docs, and one deployed service URL.
- Terminal tabs ready for
render services,render logs, andrender workflows.
Tell learners up front: no LLM provider API key is required. This removes the single biggest source of "it doesn't work for me."
- Mock model is the default. With no API key set, every deploy and test runs on a deterministic mock — totally offline, totally reproducible.
- Public PRs as input. GitHub's unauthenticated rate limit is generous enough for a room of 30.
- Pre-deployed reference instance. Deploy all three patterns from your facilitator fork before the session and keep the URLs bookmarked. If a learner's deploy stalls, they can point at yours.
AGENT_MODEL=mockas escape hatch. If a live model misbehaves mid-demo, switch and keep moving.
| Symptom | Fix | Time |
|---|---|---|
| No fork yet | Fork now; run the setup-attendee Action while npm install runs |
2 min |
npm install fails |
Check Node version — need >= 22.12. nvm install 22 |
2 min |
| Render CLI not installed | brew install render (macOS) or npm install -g @render-oss/cli |
1 min |
| Can't connect Git provider in Render | Pair with a neighbor; defer to break | 0 min |
| Blueprint names collide | They didn't run setup-attendee.yml. Run npm run setup locally, commit, push |
1 min |
Rule of thumb: if an attendee isn't unblocked within 3 minutes, pair them with a neighbor and circle back at the break.
Setup render login → render workspace set → npm install → draw the spine
Pattern 1 Blueprint deploy → live URL → submit PR → show spans/logs → break it
Pattern 2 Blueprint deploy → submit PRs → tail worker logs → scale worker
→ open kv.ts: "this is the price"
── break ──
Pattern 3 Blueprint web+DB → workflows create → run code-review → show trace
→ side-by-side fan-out table
Lab preview your-review → compose agent → force retry → ship live
Close re-draw spine: "the agent never changed"
- Talk track: "We're going to build a code reviewer once and run it three ways. Watch what doesn't change." Draw the spine.
- Pitfall: learners without Git provider access in Render. Pair them with a helper or have them follow the facilitator deploy while they keep coding locally.
- CFU: "Which folder holds the agent itself?" (
shared/agent)
- Talk track: "Simplest thing that works: the agent runs inside the request."
- On stage: after the first successful review, show
server.ts— everyawaitblocks the HTTP connection. Read the file's top comment aloud. - Break it on stage: submit a large PR → the request blocks. "What happens if I redeploy mid-review?" → in-flight work is lost. This motivates Pattern 2.
- Pitfall: a big PR on a real model can genuinely time out. That's the point, but switch to a small PR to keep pace.
- CFU: "Name two reasons this design fails under load." (timeouts, lost on deploy/crash, no independent scale.)
- Talk track: "Same building blocks — same prepareDiff, same agents, same judge. The web tier becomes a thin producer. A background worker consumes a Valkey queue and runs the same pipeline out-of-band."
- The aha: open
worker.tsand compare side-by-side withnaive-agent/server.ts. Same imports, same pipeline. Only where it runs changed. - Now flip it: open
kv.tsand scroll slowly. "This is the price. The stream, the consumer group, blocking reads, acks, retry-on-failure, the pub/sub progress bus — all of this is coordination code you now own and debug." - Pitfall: the web app can open before the worker is ready. Check service health and worker logs first.
- CFU: "What did we have to add, and what did we change in the agent?" (Added: queue/worker/acks/pub-sub. Changed in agent: nothing.)
Break here between sessions.
-
Talk track: "Same fan-out, expressed as Render tasks. The queue, retries, coordination, and observability you hand-rolled are now declarative. The unit you author is a task: a plain async function + a config object."
-
Show the code:
code-review/index.ts— each reviewer is atask()wrappingagent.run().Promise.allfans out.uxis conditional. -
The aha — the fan-out table (this is the punchline):
Pattern How fan-out is written You maintain naive Promise.all([...])in one processnothing, but no scale/durability worker XADD→ consumer group → acks → pub/subthe whole queue workflow Promise.all([agent.run(), ...])whereagentis atask()nothing -
Pitfall: empty task list → workflow didn't auto-discover. Must be
src/workflows/<name>/index.tsexporting atask(). -
Pitfall: module resolution errors → root directory/commands wrong. Root should be
packages/workflow-agents; build/start commandscd ../..first. -
CFU: "Where are the retries in Pattern 3?" (In the task's config object.)
Now coding agents come out.
- Starter:
your-review/index.tsis a working sandbox, auto-discovered. - Sequence:
- Preview:
render workflows tasks list --local→ runyour-review. - Compose an agent as a task. Encourage coding agents (Cursor/Claude/etc.) — point them at the ideas at the bottom of the file.
- Force a retry. Add
if (Math.random() < 0.5) throw new Error("flaky!"). Watch Render retry with no try/catch. Remove when done. - Bonus — fan out with
Promise.all(mirrorscode-review). - Ship it live. Push, release a version, start the task, open the trace.
- Preview:
- Pacing:
- ~5 min on step 1. Buffer for stragglers.
- ~12 min on steps 2–3 (the core). Circulate.
- 15-min mark: room check. If <half have a nested task, walk through step 2.
- 20-min mark: "5 minutes for core steps." Steps 4–5 are stretch goals.
- Hint ladder:
- "
import { securityReviewer } from '@workshop/agent'." - "Wrap in
task():const securityTask = task({ name: 'security' }, async (input) => securityReviewer.run(input, { tracer: storeTracer() }))." - "Call it:
const review = await securityTask({ patches: filtered.patches })." - "For fan-out: one
task()per reviewer, thenPromise.all. Seecode-review/index.ts."
- "
- Common bug: forgetting to import
taskfrom@renderinc/sdk/workflowsorstoreTracerfrom@workshop/db. - Coding agent tip: point learners at
.agents/skills/— therender-workflowsskill is useful fortask()patterns. - The aha (say this): "You just added durable, retried, isolated, traced, parallel execution by writing a plain function and a config object. In Pattern 2 that took a queue, consumer group, acks, and pub/sub. The agent never changed."
- CFU: "What's the difference between a step and a task?" (A step is a plain
function. A task is wrapped in
task()for isolation/retries/traces.)
- Name what they built and shipped. They have a running fork — this is code they own.
- Point at future iterations: eval harness, guardrails, circuit breakers. "More steps, tasks, budgets, tracers. Still the same agent."
- The fork is the handoff. Mock model means they can keep going with zero credentials.
Adjust the start time to your slot. Everything else shifts. Modules marked with a flex icon (~) can be shortened by the amount shown if you're running behind. Modules marked with a lock icon (!) should never be cut — they carry the core payoff.
Example: 9:00 AM start, 90-minute slot
| Clock | Dur | Module | Flex | Notes |
|---|---|---|---|---|
| 9:00 AM | 10 min | Module 0 — Setup & framing | ~ can cut to 7 min | Draw the spine. Confirm render login. |
| 9:10 AM | 15 min | Module 1 — Pattern 1 | ~ can cut to 10 min | Deploy, submit PR, show spans, break it on stage. |
| 9:25 AM | 20 min | Module 2 — Pattern 2 | ~ can cut to 15 min | Deploy, tail worker logs, trace acks, scale, open kv.ts. |
| 9:45 AM | 10 min | Break | ~ can cut to 0 | Skip if running behind — but people need it. |
| 9:55 AM | 20 min | Module 3 — Pattern 3 | ~ can cut to 12 min | Blueprint + CLI Workflow, run task, show trace, fan-out table. |
| 10:15 AM | 25 min | Lab — Author a task | ! never cut below 20 min | The finale. Steps 1–3 minimum; 4–5 if time allows. |
| 10:40 AM | 10 min | Module 4 — Close | ~ can cut to 5 min | Re-draw spine, exit ticket, point at future iterations. |
| 10:50 AM | — | End |
If you're 10+ min behind at the break: cut Module 3 to 12 min (skip the Dashboard walkthrough — just show the CLI and the trace) and start the Lab with "steps 1–3 only, ship-live is homework." Protect the Lab's minimum 20 min — it's the reason they came.
Transition cues (say these at each boundary):
- Setup → Pattern 1: "Now let's see the simplest version live."
- Pattern 1 → Pattern 2: "That broke. Let's fix durability — but watch what it costs."
- Pattern 2 → Break: "That coordination is real. Hold onto that feeling."
- Break → Pattern 3: "Now watch all of that become a config object."
- Pattern 3 → Lab: "Your turn — bring out your coding agents."
- Lab → Close: "Let's zoom out. What changed in the agent? Nothing."
import { task } from "@renderinc/sdk/workflows";
import { securityReviewer } from "@workshop/agent";
import { storeTracer } from "@workshop/db";
const securityTask = task(
{ name: "security", timeoutSeconds: 120 },
async (input: { patches: Patch[] }, runId?: string) => {
return securityReviewer.run(input, { tracer: storeTracer(), runId });
},
);
// inside yourReview, after you have filtered.patches:
const review = await securityTask({ patches: filtered.patches });
return { ...existingReturn, review: review.text };Bonus (fan out both reviewers):
import { REVIEWERS } from "@workshop/agent";
const reviews = await Promise.all(
REVIEWERS.map((agent) =>
task(
{ name: agent.name },
async (input: { patches: Patch[] }) => agent.run(input, { tracer: storeTracer() }),
)({ patches: filtered.patches }),
),
);Quick checks that learners hit the objectives (use any 2–3):
- "Give one failure mode of Pattern 1 and the Pattern 2 feature that fixes it."
- "What two things make up a Render
task()?" (a config object + an async fn) - "Where did the retry logic live in Pattern 2 vs Pattern 3?"