Render · Hosting, Workflows, Cron

Sellers who never ship, buyers who go quiet, holds that get old: real deals stall. Handshake runs on Render, and a Render Workflow enforces every deadline automatically, even when nobody has the page open.

How a deadline gets enforced

Render Cron

every 5 min

sweepDeals

Workflow task

enforceDeadline × N

one run per deal

Handshake app

re-checks, then acts

PayPal

void · capture · renew

Real run · PayPal sandbox · deadlines sped up for the test

sweep 13 deadlines due across 2 deals
SHIP_BYSeller never shipped → hold voided, buyer paid nothing
REAUTHORIZEPayPal's honor period not over → rescheduled, no error
AUTO_RELEASEBuyer went quiet → $230 captured, seller paid $223.55
sweep 20 due → nothing runs twice

The deadlines

Four timers run on every protected deal. Both people see a live countdown, and the workflow acts the moment one runs out.

  1. 01

    Ship within 3 days

    The seller has three days from payment to ship. If they don't, the PayPal hold is voided and the buyer pays nothing.

    Protects the buyer
  2. 02

    Renew the hold on day 3

    PayPal guarantees a hold for three days. Handshake reauthorizes it then, so the money stays reserved while the item travels.

    PayPal reauthorize
  3. 03

    Respond within 7 days of shipping

    If the buyer neither confirms nor reports a problem, payment is captured and the seller is paid, but only when the seller's label photo matched the buyer's PayPal address. A typed tracking number never triggers it.

    Protects the seller
  4. 04

    Settle claims within 3 days

    If nobody declines a damage proposal, the code-computed refund settles automatically. A decline sends it to PayPal instead.

    Protects both

Built to move money safely

Background jobs that move money have one job: never do it twice. Every layer is built around that.

The app re-checks every time

The workflow decides when to act. The app checks the deadline again inside the deal's lock, so a retry after a success does nothing.

Retries only when they help

Render retries a failed run with backoff. Only transient failures (a PayPal 5xx, a deploy in progress) throw; a bad request is reported, not retried.

One sweep per tick

Each cron tick starts the sweep with an idempotency key, so a retried tick can't start a second one.

A secret nobody types

The app and the workflow share a secret that Render generates in a Blueprint environment group. It never appears in code or chat.

Settlement resumes

Capture, tracking and payout are recorded step by step. If the payout fails, the retry pays the seller without capturing twice.

Demo speed, same rules

One setting shrinks days to minutes for demos. The order of events and every safety check stay exactly the same.

The whole stack

Everything is defined in a single render.yaml, so the whole system deploys from one file.

  1. 01

    Web service

    The Next.js app and its API: deal pages, PayPal calls, AI checks. On the Starter plan, so it never sleeps and a judge's first click is instant.

    Starter
  2. 02

    Persistent disk

    Deals live on a 1 GB disk, written atomically, so they survive redeploys and restarts.

    1 GB disk
  3. 03

    Tests gate every deploy

    Render runs the money-rule tests before each build. If pricing, refund caps, negotiation limits or timers break, the deploy stops and the live site keeps working.

    npm test
  4. 04

    Workflow + cron

    A Render Workflow with two tasks enforces deadlines, started every five minutes by a Render cron job.

    Workflows

Proof

3

timer actions run against PayPal's sandbox: a void, a capture with payout, and a rescheduled renewal.

End to end

0

duplicate money moves, across repeated sweeps and three simultaneous runs of the same deadline.

Idempotency

327

automated tests: 319 for the app, run on both the file store and Postgres before every deploy, and 8 for the workflow. Plus 44 live scenarios on the sandbox.

Vitest + live runs

What we use

sweepDeals task

task({ name: 'sweepDeals' })

Asks the app which deals are past a deadline and fans out one run per deal with ctx.run.

enforceDeadline task

retry: { maxRetries: 4, backoffScaling: 2 }

Each deadline runs on its own instance, retried with exponential backoff on transient failures.

Starting runs

render.workflows.startTask()

The cron job starts each sweep with an idempotency key, so a retried tick can't double up.

Cron job

schedule: "*/5 * * * *"

Render Workflows don't schedule themselves yet, so a cron job starts the sweep every five minutes.

Blueprint env group

generateValue: true

Render generates the shared secret the app uses to trust the workflow.

Persistent disk

disk: mountPath /var/data

Deals and their clocks survive deploys, so no deadline is ever lost.