Skip to content

feat: add fullstack-vercel-turso example#105

Merged
coji merged 10 commits into
mainfrom
feat/example-fullstack-vercel-turso
Mar 12, 2026
Merged

feat: add fullstack-vercel-turso example#105
coji merged 10 commits into
mainfrom
feat/example-fullstack-vercel-turso

Conversation

@coji
Copy link
Copy Markdown
Owner

@coji coji commented Mar 8, 2026

Summary

  • Add fullstack-vercel-turso example demonstrating Durably on Vercel serverless with Turso (libSQL)
  • onRequest lazy init pattern for real-time SSE streaming during request lifetime
  • Vercel Cron background worker (/api/worker) for processing jobs after SSE disconnects
  • Long-running "Report Generation" job (~90s, 21 steps) that intentionally exceeds Vercel's serverless timeout to showcase step resumability across invocations
  • Local development with libsqld via Docker (same HTTP protocol as production Turso)

Architecture

User triggers job → server action → durably.jobs.*.trigger()
                  ↓
SSE subscribe     → onRequest → durably.init() starts worker
                  ↓
Worker processes  → steps stream via SSE (until serverless timeout)
                  ↓
Timeout           → SSE disconnects, steps persist in Turso
                  ↓
Vercel Cron (1m)  → POST /api/worker → processUntilIdle()
                  ↓
Remaining steps   → processed by cron, final result persisted

Test plan

  • pnpm --filter example-fullstack-vercel-turso typecheck passes
  • pnpm validate passes
  • Local dev works with Docker libsqld
  • Report generation job demonstrates SSE streaming + cron resumption

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Fullstack example with resumable background jobs, client triggers, per-run progress UI, interactive dashboard with live SSE updates, and forms for image, data-sync, CSV import, and report generation; serverless job runtime and a worker endpoint (cron-invoked).
  • Documentation

    • New README and example environment file with local and production setup guidance.
  • Chores

    • Local Docker Compose, ignore files, project configs (Vite/TS/Prettier/react-router), and package/tooling setup added.

coji and others added 2 commits March 8, 2026 22:47
Demonstrates Durably on Vercel serverless with Turso (libSQL):
- onRequest lazy init for real-time SSE streaming
- Vercel Cron background worker for processing interrupted jobs
- Long-running report generation job (~90s, 21 steps) that exceeds
  serverless timeout to showcase step resumability across invocations
- Local development with libsqld via Docker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
durably-demo Ready Ready Preview Mar 12, 2026 1:18pm
durably-demo-vercel-turso Ready Ready Preview Mar 12, 2026 1:18pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a fullstack Vercel + Turso example under examples/fullstack-vercel-turso/ including React Router frontend, Durably job definitions and client/server wiring, Turso/libSQL DB config, Docker/dev and Vercel configs, SSE progress streaming, and a job runs dashboard.

Changes

Cohort / File(s) Summary
Project config & tooling
examples/fullstack-vercel-turso/.dockerignore, examples/fullstack-vercel-turso/.gitignore, examples/fullstack-vercel-turso/.env.example, examples/fullstack-vercel-turso/biome.json, examples/fullstack-vercel-turso/prettier.config.js, examples/fullstack-vercel-turso/tsconfig.json, examples/fullstack-vercel-turso/package.json, examples/fullstack-vercel-turso/vite.config.ts, examples/fullstack-vercel-turso/react-router.config.ts, examples/fullstack-vercel-turso/vercel.json, examples/fullstack-vercel-turso/docker-compose.yml, examples/fullstack-vercel-turso/.vercelignore
Adds example project configs: ignore patterns, env example, Biome/Prettier/TS settings, Vite/React Router/Tailwind plugins, Docker Compose for libsqld, and Vercel cron + ignore rules.
Styling
examples/fullstack-vercel-turso/app/app.css
Adds Tailwind import, theme font stack, and base apply rules.
Durably server & DB libs
examples/fullstack-vercel-turso/app/lib/database.server.ts, examples/fullstack-vercel-turso/app/lib/durably.server.ts
Adds Turso/libSQL dialect init and a lazily-initialized Durably server instance with registered jobs and an HTTP handler.
Durably client lib
examples/fullstack-vercel-turso/app/lib/durably.ts
Adds a type-safe Durably client configured to call /api/durably.
Job definitions & index
examples/fullstack-vercel-turso/app/jobs/data-sync.ts, examples/fullstack-vercel-turso/app/jobs/generate-report.ts, examples/fullstack-vercel-turso/app/jobs/import-csv.ts, examples/fullstack-vercel-turso/app/jobs/process-image.ts, examples/fullstack-vercel-turso/app/jobs/index.ts, examples/fullstack-vercel-turso/app/jobs/delay.ts
Introduces five durable jobs/util (data-sync, generate-report, import-csv, process-image, delay) with input/output schemas, multi-step/resumable logic, progress reporting, exported types, and a barrel index with typed aliases.
Routes & API
examples/fullstack-vercel-turso/app/routes.ts, examples/fullstack-vercel-turso/app/routes/api.durably.$.ts, examples/fullstack-vercel-turso/app/routes/api.worker.ts
Adds route manifest and API endpoints: catch-all Durably API delegating to shared handler and /api/worker that authenticates via CRON_SECRET and processes pending jobs.
Root & Error UI
examples/fullstack-vercel-turso/app/root.tsx
Adds app root layout, links, and an ErrorBoundary with dev stack output.
Index route & Dashboard
examples/fullstack-vercel-turso/app/routes/_index.tsx, examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx
Adds index route with a server action to trigger jobs and a Dashboard UI showing run history, SSE updates, pagination, and run actions.
Job forms & progress UI
examples/fullstack-vercel-turso/app/routes/_index/data-sync-form.tsx, examples/fullstack-vercel-turso/app/routes/_index/data-sync-progress.tsx, examples/fullstack-vercel-turso/app/routes/_index/image-processing-form.tsx, examples/fullstack-vercel-turso/app/routes/_index/image-processing-progress.tsx, examples/fullstack-vercel-turso/app/routes/_index/report-form.tsx, examples/fullstack-vercel-turso/app/routes/_index/report-progress.tsx, examples/fullstack-vercel-turso/app/routes/_index/run-progress.tsx
Adds job trigger forms and progress components that subscribe to Durably client hooks; RunProgress renders status, progress, logs, output, and errors.
Documentation
examples/fullstack-vercel-turso/README.md, CLAUDE.md
Adds a README documenting the example architecture, setup, and deployment; updates CLAUDE.md with documentation additions.
Workspace-level .gitignore
.gitignore
Adds .vercel and .env*.local ignore patterns to repository .gitignore.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Browser
    participant Index as Index Route<br/>(Server Action)
    participant API as Durably API<br/>(/api/durably)
    participant Durably as Durably<br/>Instance
    participant DB as Turso<br/>Database
    User->>Index: Submit job form (intent + params)
    Index->>Durably: durably.jobs.X.trigger(input)
    Durably-->>Index: runId
    Index-->>User: Return runId
    User->>API: SSE subscribe /api/durably/subscribe?runId=X
    API->>Durably: Subscribe to run X
    Durably->>DB: Persist/read run state & checkpoints
    Durably->>Durably: Execute job steps, emit progress/logs
    Durably-->>API: Progress events
    API-->>User: Stream SSE events (progress, logs, status)
    API-->>User: Final status when complete
Loading
sequenceDiagram
    participant Cron as Vercel Cron
    participant Worker as /api/worker Endpoint
    participant Handler as durablyHandler
    participant Durably as Durably Instance
    participant DB as Turso Database
    Cron->>Worker: POST /api/worker (Authorization: Bearer CRON_SECRET)
    Worker->>Handler: durablyHandler.handle(request)
    Handler->>Durably: durably.init()
    Handler->>Durably: durably.processUntilIdle()
    Durably->>DB: Fetch pending jobs
    loop For each pending job
        Durably->>Durably: Execute job steps and update checkpoints
        Durably->>DB: Persist progress/status
    end
    Durably-->>Handler: { processed: N }
    Handler-->>Worker: HTTP 200 { processed: N }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Poem

🐇 I hopped through code beneath the moon,
Jobs queued and hummed and finished soon,
Turso burrowed carrots deep,
Durably wakes while others sleep,
A tiny rabbit cheers—hooray, zoom!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add fullstack-vercel-turso example' accurately and directly describes the main change: adding a new example project demonstrating Durably on Vercel with Turso, which is the primary focus of this comprehensive PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/example-fullstack-vercel-turso
📝 Coding Plan for PR comments
  • Generate coding plan

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
examples/fullstack-vercel-turso/.dockerignore (1)

1-4: Consider removing this unused file.

This .dockerignore file has no effect since this example uses Vercel serverless deployment (no Dockerfile). The docker-compose.yml is only used to run libsqld locally, and .dockerignore only affects docker build contexts, not docker compose up.

This appears to be copied from fullstack-react-router which does have a Dockerfile.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/.dockerignore` around lines 1 - 4, The
.dockerignore file is unused for this Vercel serverless example (no Dockerfile)
and should be removed to avoid confusion; delete the .dockerignore entry from
the example repo (or if there is an intentional reason to keep it, add a brief
note in the example README explaining why it’s present and referencing
docker-compose usage for local libsqld only).
examples/fullstack-vercel-turso/app/routes/api.worker.ts (1)

24-25: Bound the cron worker's batch size.

processUntilIdle() keeps polling until the queue is empty. Under backlog, that can consume the whole serverless budget for one cron tick and get the request terminated before it returns. Consider setting an explicit cap per invocation and letting the next scheduled run continue the queue.

Possible change
-  const processed = await durably.processUntilIdle()
+  const processed = await durably.processUntilIdle({ maxRuns: 1 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/routes/api.worker.ts` around lines 24 -
25, processUntilIdle() will drain the entire queue in one invocation and can
exhaust serverless time on backlog; modify the cron handler using the durably
client (durably) to enforce a per-invocation cap: replace the single await
durably.processUntilIdle() call with a bounded processing loop or a single call
to a batch-oriented API (e.g., durably.processBatch or
durably.processUntilIdle({...maxJobs: N...}) if supported) and stop after a
fixed number of jobs (use the processed counter returned to break when you hit
the cap) so the remaining work is picked up by the next scheduled run.
examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx (1)

12-27: Add generate-report to DashboardRun.

useRuns<DashboardRun>() is modeling the runs rendered in this table, but the union omits the new report job. That leaves the headline flow for this example outside the typed dashboard contract.

♻️ Suggested update
 import type {
   DataSyncInput,
   DataSyncOutput,
+  GenerateReportInput,
+  GenerateReportOutput,
   ImportCsvInput,
   ImportCsvOutput,
   ProcessImageInput,
   ProcessImageOutput,
 } from '~/jobs'
@@
 type DashboardRun =
   | TypedClientRun<DataSyncInput, DataSyncOutput>
+  | TypedClientRun<GenerateReportInput, GenerateReportOutput>
   | TypedClientRun<ImportCsvInput, ImportCsvOutput>
   | TypedClientRun<ProcessImageInput, ProcessImageOutput>

Also applies to: 46-49

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines
12 - 27, Update the DashboardRun union to include the new "generate-report" job:
add a union member using TypedClientRun with the generate-report job's
input/output types (e.g., TypedClientRun<GenerateReportInput,
GenerateReportOutput>) so useRuns<DashboardRun>() correctly types the report
runs; update the same pattern wherever DashboardRun is declared/used (the union
near the DashboardRun type and the duplicate occurrence referenced in the
review).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/fullstack-vercel-turso/app/jobs/data-sync.ts`:
- Around line 12-56: The job definition for dataSyncJob uses defineJob but
doesn’t set preserveSteps, so step history will be cleaned up; update the
defineJob call for dataSyncJob to include preserveSteps: true in its
configuration object (and apply the same flag to other example jobs) so the
dashboard can retain step records for completed runs — locate the dataSyncJob
definition and add the preserveSteps: true property alongside
name/input/output/run.

In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts`:
- Around line 45-46: The progress ordinal resets because currentStep is
reinitialized to 0 each run; instead use fixed ordinals per step so resumed jobs
don't regress UI. Update all step.run() blocks (the ones using currentStep and
totalSteps) to compute and use a fixed step index (e.g., a constant ordinal for
each step) when publishing progress rather than incrementing a local currentStep
on each invocation; ensure the step.run() callbacks read persisted step outputs
to determine completed steps and publish progress as "ordinal/totalSteps" using
the fixed ordinal values for each specific step (reference totalSteps,
currentStep, and step.run()).

In `@examples/fullstack-vercel-turso/app/jobs/import-csv.ts`:
- Around line 72-75: The run log currently prints PII (row.name, row.email,
row.amount) in the import job; update the logging in the import-csv.ts job to
avoid sensitive data by changing the step.log.info call to output a
non-sensitive identifier (e.g., row.id or a generated import index) and a
generic status message instead, leaving the import counter (imported) and import
logic unchanged; locate the step.log.info usage that references
row.name/row.email/row.amount and replace those fields with row.id or "row
index" and a non-sensitive message.

In `@examples/fullstack-vercel-turso/README.md`:
- Around line 15-27: The fenced ASCII flow block lacks a language hint and
triggers markdownlint MD040; update the triple-backtick fence surrounding the
flow (the block showing "User triggers job → POST /api/durably/trigger" through
"Vercel Cron → POST /api/worker") to use a simple language tag like text
(replace ``` with ```text at the start and close with ```), ensuring the fenced
block is labeled and passes validation.

---

Nitpick comments:
In `@examples/fullstack-vercel-turso/.dockerignore`:
- Around line 1-4: The .dockerignore file is unused for this Vercel serverless
example (no Dockerfile) and should be removed to avoid confusion; delete the
.dockerignore entry from the example repo (or if there is an intentional reason
to keep it, add a brief note in the example README explaining why it’s present
and referencing docker-compose usage for local libsqld only).

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx`:
- Around line 12-27: Update the DashboardRun union to include the new
"generate-report" job: add a union member using TypedClientRun with the
generate-report job's input/output types (e.g.,
TypedClientRun<GenerateReportInput, GenerateReportOutput>) so
useRuns<DashboardRun>() correctly types the report runs; update the same pattern
wherever DashboardRun is declared/used (the union near the DashboardRun type and
the duplicate occurrence referenced in the review).

In `@examples/fullstack-vercel-turso/app/routes/api.worker.ts`:
- Around line 24-25: processUntilIdle() will drain the entire queue in one
invocation and can exhaust serverless time on backlog; modify the cron handler
using the durably client (durably) to enforce a per-invocation cap: replace the
single await durably.processUntilIdle() call with a bounded processing loop or a
single call to a batch-oriented API (e.g., durably.processBatch or
durably.processUntilIdle({...maxJobs: N...}) if supported) and stop after a
fixed number of jobs (use the processed counter returned to break when you hit
the cap) so the remaining work is picked up by the next scheduled run.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1ad8a074-d6ae-4c8f-8fc7-6bf465115bdb

📥 Commits

Reviewing files that changed from the base of the PR and between c4bb49b and 8fec299.

⛔ Files ignored due to path filters (2)
  • examples/fullstack-vercel-turso/public/favicon.ico is excluded by !**/*.ico
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (34)
  • examples/fullstack-vercel-turso/.dockerignore
  • examples/fullstack-vercel-turso/.env.example
  • examples/fullstack-vercel-turso/.gitignore
  • examples/fullstack-vercel-turso/README.md
  • examples/fullstack-vercel-turso/app/app.css
  • examples/fullstack-vercel-turso/app/jobs/data-sync.ts
  • examples/fullstack-vercel-turso/app/jobs/generate-report.ts
  • examples/fullstack-vercel-turso/app/jobs/import-csv.ts
  • examples/fullstack-vercel-turso/app/jobs/index.ts
  • examples/fullstack-vercel-turso/app/jobs/process-image.ts
  • examples/fullstack-vercel-turso/app/lib/database.server.ts
  • examples/fullstack-vercel-turso/app/lib/durably.server.ts
  • examples/fullstack-vercel-turso/app/lib/durably.ts
  • examples/fullstack-vercel-turso/app/root.tsx
  • examples/fullstack-vercel-turso/app/routes.ts
  • examples/fullstack-vercel-turso/app/routes/_index.tsx
  • examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx
  • examples/fullstack-vercel-turso/app/routes/_index/data-sync-form.tsx
  • examples/fullstack-vercel-turso/app/routes/_index/data-sync-progress.tsx
  • examples/fullstack-vercel-turso/app/routes/_index/image-processing-form.tsx
  • examples/fullstack-vercel-turso/app/routes/_index/image-processing-progress.tsx
  • examples/fullstack-vercel-turso/app/routes/_index/report-form.tsx
  • examples/fullstack-vercel-turso/app/routes/_index/report-progress.tsx
  • examples/fullstack-vercel-turso/app/routes/_index/run-progress.tsx
  • examples/fullstack-vercel-turso/app/routes/api.durably.$.ts
  • examples/fullstack-vercel-turso/app/routes/api.worker.ts
  • examples/fullstack-vercel-turso/biome.json
  • examples/fullstack-vercel-turso/docker-compose.yml
  • examples/fullstack-vercel-turso/package.json
  • examples/fullstack-vercel-turso/prettier.config.js
  • examples/fullstack-vercel-turso/react-router.config.ts
  • examples/fullstack-vercel-turso/tsconfig.json
  • examples/fullstack-vercel-turso/vercel.json
  • examples/fullstack-vercel-turso/vite.config.ts

Comment on lines +12 to +56
export const dataSyncJob = defineJob({
name: 'data-sync',
input: z.object({ userId: z.string() }),
output: z.object({ synced: z.number(), failed: z.number() }),
run: async (step, input) => {
step.log.info(`Starting sync for user: ${input.userId}`)

const items = await step.run('fetch-local', async () => {
step.progress(1, 4, 'Fetching local data...')
await delay(300)
return Array.from({ length: 10 }, (_, i) => ({
id: `item-${i}`,
data: `Data for ${input.userId}`,
}))
})

let synced = 0
let failed = 0

for (let i = 0; i < items.length; i++) {
const item = items[i]
const success = await step.run(`sync-item-${item.id}`, async () => {
step.progress(2 + Math.floor(i / 5), 4, `Syncing item ${i + 1}...`)
await delay(100)
return Math.random() > 0.1 // 90% success rate
})

if (success) {
synced++
} else {
failed++
step.log.warn(`Failed to sync item: ${item.id}`)
}
}

await step.run('finalize', async () => {
step.progress(4, 4, 'Finalizing...')
await delay(200)
})

step.log.info(`Sync complete: ${synced} synced, ${failed} failed`)

return { synced, failed }
},
})
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e

# 1) Show where the dashboard fetches and renders step history.
rg -n -C2 'getSteps\(|Steps:' examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx

echo
# 2) Show preserveSteps / cleanupSteps semantics in the runtime or docs.
rg -n -C3 'preserveSteps|cleanupSteps' packages/durably/src docs || true

echo
# 3) Show whether the example jobs opt into step retention.
rg -n -C2 'preserveSteps' examples/fullstack-vercel-turso/app/jobs || true

Repository: coji/durably

Length of output: 4881


Add preserveSteps: true to preserve step history for the dashboard.

The dashboard fetches and displays step history for completed runs, but this job uses the default cleanup behavior that deletes steps when runs reach a terminal state. Add preserveSteps: true to the job definition to preserve step records, and apply the same setting to the other example jobs the dashboard displays.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/jobs/data-sync.ts` around lines 12 - 56,
The job definition for dataSyncJob uses defineJob but doesn’t set preserveSteps,
so step history will be cleaned up; update the defineJob call for dataSyncJob to
include preserveSteps: true in its configuration object (and apply the same flag
to other example jobs) so the dashboard can retain step records for completed
runs — locate the dataSyncJob definition and add the preserveSteps: true
property alongside name/input/output/run.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional — we don't want to preserve steps for this demo. Step data is cleaned up on terminal state by design (default behavior). The dashboard's View button fetches steps, which will show empty for completed runs, and that's fine for demonstrating the default.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coji, understood! Demonstrating the default cleanup behavior is a perfectly valid choice for this example — showing that step history is empty for completed runs is itself informative for users exploring the defaults.


✏️ Learnings added
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/data-sync.ts:11-55
Timestamp: 2026-03-12T13:18:37.201Z
Learning: In `examples/fullstack-vercel-turso`, all example jobs (dataSyncJob, generateReportJob, importCsvJob, processImageJob) intentionally omit `preserveSteps: true` to demonstrate the default behavior where step history is deleted when runs reach a terminal state. The dashboard's View button showing empty steps for completed runs is acceptable and by design. Do not suggest adding `preserveSteps: true` to these jobs.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 101
File: docs/rfcs/runtime-rearchitecture/ja/core-runtime.md:0-0
Timestamp: 2026-03-08T05:06:39.050Z
Learning: Reviewers should verify that step execution uses step.run() and that step output data is persisted. By default, preserveSteps is false (step history is deleted when runs reach a terminal state). If audit/debug history is required, ensure preserveSteps is set to true. Be aware that cleanupSteps used to exist with inverted semantics (cleanupSteps: true meant delete), and this was renamed to preserveSteps in Phase 1. Apply this guidance to all JS/TS code and docs that reference step persistence.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.

Comment thread examples/fullstack-vercel-turso/app/jobs/generate-report.ts Outdated
Comment thread examples/fullstack-vercel-turso/app/jobs/import-csv.ts
Comment thread examples/fullstack-vercel-turso/README.md Outdated
- Replace mutable currentStep counter with fixed step numbers in
  generate-report job. Step callbacks are skipped on resume, so a
  mutable counter would report wrong progress after restart.
- Add GenerateReportInput/Output to DashboardRun union type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts`:
- Around line 39-42: The department field on the zod input (input: z.object({
reportType, department })) is used raw when building reportUrl, so encode the
department before interpolation to avoid breaking the URL; update the code that
constructs reportUrl (the interpolation using reportUrl) to use an encoded
department value (e.g., apply a path-safe encoding like encodeURIComponent or a
dedicated path-segment encoder) and/or add a zod transform/validation on
department to ensure it’s safely encoded, ensuring all places that interpolate
department (the reportUrl construction) use the encoded value.
- Line 50: Replace direct step.log.info calls and non-persisted timestamp
assignments with persisted step.run() checkpoints: wrap the "Starting
${input.reportType} report..." log and the completion log/ generatedAt
assignment inside step.run() calls so the start and completion metadata (logs
and generatedAt) are written to persistence; specifically, change the code that
currently uses step.log.info(...) for the start and the block that sets
generatedAt and logs completion to instead call step.run({/* metadata or return
value */}) or pass the log/timestamp as the step output, using the existing
step.run() API to persist these values so resumed runs won’t duplicate the
"Starting ..." entry or mis-stamp generatedAt.

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx`:
- Around line 82-89: In showDetails, avoid flashing the previous run’s steps by
clearing steps before switching the selected run and while fetching new steps:
call setSteps([]) immediately when a run is found (before setSelectedRun or
before awaiting getSteps), then await getSteps(runId) and setSteps with the
result; update the sequence around getRun, setSelectedRun, setSteps, and
getSteps to ensure steps are reset first and handle any getSteps errors before
setting the new steps.
- Around line 279-289: The modal overlay is only visual; add proper dialog
semantics by giving the dialog container (the element with className "mx-4
max-h-[80vh] w-full max-w-2xl ...") role="dialog" and aria-modal="true", add an
id to the <h3> (e.g., run-details-title) and reference it via aria-labelledby on
the dialog, and ensure the close button (the button that calls
setSelectedRun(null)) has an accessible name (e.g., aria-label="Close run
details" or aria-labelledby) so assistive tech receives a proper modal context.
- Around line 213-223: The retrigger button is only rendered when run.status is
'failed' or 'cancelled', but the API supports 'completed' runs too; update the
conditional that controls rendering (the JSX block using run.status and the
onClick handler handleRetrigger) to also include 'completed' (e.g., allow
run.status === 'completed' or change to permit statuses other than
'running'/'pending' as appropriate) so completed runs show the "Retrigger"
button and remain disabled when isActioning is true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc00174f-ed3c-45ca-895a-48927bfb38cf

📥 Commits

Reviewing files that changed from the base of the PR and between 8fec299 and 1359e95.

📒 Files selected for processing (2)
  • examples/fullstack-vercel-turso/app/jobs/generate-report.ts
  • examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx

Comment on lines +39 to +42
input: z.object({
reportType: z.enum(['daily', 'weekly', 'monthly']),
department: z.string(),
}),
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Encode department before interpolating it into reportUrl.

Lines 39-42 accept any department string, but Line 204 uses it as a raw path segment. Values containing /, ?, #, or spaces will change the returned URL shape.

🔧 Proposed fix
-      const url = `https://reports.example.com/${input.department}/${input.reportType}-${Date.now()}.pdf`
+      const departmentPath = encodeURIComponent(input.department)
+      const url = `https://reports.example.com/${departmentPath}/${input.reportType}-${Date.now()}.pdf`

Also applies to: 201-205

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts` around lines 39
- 42, The department field on the zod input (input: z.object({ reportType,
department })) is used raw when building reportUrl, so encode the department
before interpolation to avoid breaking the URL; update the code that constructs
reportUrl (the interpolation using reportUrl) to use an encoded department value
(e.g., apply a path-safe encoding like encodeURIComponent or a dedicated
path-segment encoder) and/or add a zod transform/validation on department to
ensure it’s safely encoded, ensuring all places that interpolate department (the
reportUrl construction) use the encoded value.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a simulated URL in a demo job — the report URL is never actually fetched or used. Skipping for now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coji, understood — since the reportUrl is a mock value purely for demo output and is never actually fetched or navigated to, URL encoding is indeed unnecessary here. Thanks for the clarification!

(ᵔᴥᵔ)ノ


✏️ Learnings added
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/generate-report.ts:36-39
Timestamp: 2026-03-12T13:19:16.662Z
Learning: In the `examples/fullstack-vercel-turso` example project, the `reportUrl` returned by `generateReportJob` (in `app/jobs/generate-report.ts`) is a simulated/mock URL used only as demo output and is never actually fetched or navigated to. URL-encoding the `department` path segment is not required in this context.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.

Learnt from: coji
Repo: coji/durably PR: 101
File: docs/rfcs/runtime-rearchitecture/ja/core-runtime.md:0-0
Timestamp: 2026-03-08T05:06:39.050Z
Learning: Reviewers should verify that step execution uses step.run() and that step output data is persisted. By default, preserveSteps is false (step history is deleted when runs reach a terminal state). If audit/debug history is required, ensure preserveSteps is set to true. Be aware that cleanupSteps used to exist with inverted semantics (cleanupSteps: true meant delete), and this was renamed to preserveSteps in Phase 1. Apply this guidance to all JS/TS code and docs that reference step persistence.

// so a mutable counter would report wrong progress after restart.
const T = 21

step.log.info(`Starting ${input.reportType} report for ${input.department}`)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Persist the start/completion metadata via step.run().

Line 50 and Line 220 are not checkpointed, so a resumed run can append an extra “Starting …” log entry and stamp a later generatedAt if the invocation dies after the last persisted step. examples/fullstack-vercel-turso/app/routes/_index/report-progress.tsx:16-26 renders logs, so that replay is visible in the example UI.

🔧 Proposed fix
-    step.log.info(`Starting ${input.reportType} report for ${input.department}`)
+    await step.run('initialize-report', async () => {
+      step.log.info(`Starting ${input.reportType} report for ${input.department}`)
+    })
...
+    const generatedAt = await step.run('finalize-report', async () => {
+      return new Date().toISOString()
+    })
+
     return {
       reportUrl,
       totalRecords,
-      generatedAt: new Date().toISOString(),
+      generatedAt,
     }

Based on learnings: step execution uses step.run() and step output data is persisted.

Also applies to: 217-220

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts` at line 50,
Replace direct step.log.info calls and non-persisted timestamp assignments with
persisted step.run() checkpoints: wrap the "Starting ${input.reportType}
report..." log and the completion log/ generatedAt assignment inside step.run()
calls so the start and completion metadata (logs and generatedAt) are written to
persistence; specifically, change the code that currently uses
step.log.info(...) for the start and the block that sets generatedAt and logs
completion to instead call step.run({/* metadata or return value */}) or pass
the log/timestamp as the step output, using the existing step.run() API to
persist these values so resumed runs won’t duplicate the "Starting ..." entry or
mis-stamp generatedAt.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The job already logs start/completion messages via step.log.info(). Adding extra metadata persistence is over-engineering for a demo.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coji, understood — keeping the demo straightforward is a valid priority. I'll note this for future reviews.


✏️ Learnings added
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/generate-report.ts:47-47
Timestamp: 2026-03-12T13:19:14.905Z
Learning: In `examples/fullstack-vercel-turso/app/jobs/generate-report.ts`, the `step.log.info()` calls for start/completion messages and the inline `new Date().toISOString()` for `generatedAt` are intentionally left outside `step.run()` checkpoints. This is by design to keep the demo simple; wrapping them in `step.run()` would be over-engineering for an example project. Do not suggest adding extra `step.run()` checkpoints for logging or metadata in this file.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/data-sync.ts:11-55
Timestamp: 2026-03-12T13:18:37.201Z
Learning: In `examples/fullstack-vercel-turso`, all example jobs (dataSyncJob, generateReportJob, importCsvJob, processImageJob) intentionally omit `preserveSteps: true` to demonstrate the default behavior where step history is deleted when runs reach a terminal state. The dashboard's View button showing empty steps for completed runs is acceptable and by design. Do not suggest adding `preserveSteps: true` to these jobs.

Learnt from: coji
Repo: coji/durably PR: 101
File: docs/rfcs/runtime-rearchitecture/ja/core-runtime.md:0-0
Timestamp: 2026-03-08T05:06:39.050Z
Learning: Reviewers should verify that step execution uses step.run() and that step output data is persisted. By default, preserveSteps is false (step history is deleted when runs reach a terminal state). If audit/debug history is required, ensure preserveSteps is set to true. Be aware that cleanupSteps used to exist with inverted semantics (cleanupSteps: true meant delete), and this was renamed to preserveSteps in Phase 1. Apply this guidance to all JS/TS code and docs that reference step persistence.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.

Comment on lines +82 to +89
const showDetails = async (runId: string) => {
const run = await getRun(runId)
if (run) {
setSelectedRun(run)
const stepsData = await getSteps(runId)
setSteps(stepsData)
}
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reset steps before switching the modal to another run.

Line 85 updates selectedRun before the new getSteps() call finishes, so the previous run’s step list can flash under the newly selected run until Line 87 completes.

💡 Minimal fix
   const showDetails = async (runId: string) => {
-    const run = await getRun(runId)
-    if (run) {
-      setSelectedRun(run)
-      const stepsData = await getSteps(runId)
-      setSteps(stepsData)
-    }
+    setSteps([])
+    const [run, stepsData] = await Promise.all([getRun(runId), getSteps(runId)])
+    if (!run) return
+    setSelectedRun(run)
+    setSteps(stepsData)
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const showDetails = async (runId: string) => {
const run = await getRun(runId)
if (run) {
setSelectedRun(run)
const stepsData = await getSteps(runId)
setSteps(stepsData)
}
}
const showDetails = async (runId: string) => {
setSteps([])
const [run, stepsData] = await Promise.all([getRun(runId), getSteps(runId)])
if (!run) return
setSelectedRun(run)
setSteps(stepsData)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines
82 - 89, In showDetails, avoid flashing the previous run’s steps by clearing
steps before switching the selected run and while fetching new steps: call
setSteps([]) immediately when a run is found (before setSelectedRun or before
awaiting getSteps), then await getSteps(runId) and setSteps with the result;
update the sequence around getRun, setSelectedRun, setSteps, and getSteps to
ensure steps are reset first and handle any getSteps errors before setting the
new steps.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — but in practice the modal is closed before opening another, so stale steps flash is unlikely. Will keep it simple for now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coji, that makes sense — if the modal is always dismissed before reopening, there's no opportunity for the stale flash to appear. Thanks for the context!


🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/data-sync.ts:11-55
Timestamp: 2026-03-12T13:18:37.201Z
Learning: In `examples/fullstack-vercel-turso`, all example jobs (dataSyncJob, generateReportJob, importCsvJob, processImageJob) intentionally omit `preserveSteps: true` to demonstrate the default behavior where step history is deleted when runs reach a terminal state. The dashboard's View button showing empty steps for completed runs is acceptable and by design. Do not suggest adding `preserveSteps: true` to these jobs.

Learnt from: coji
Repo: coji/durably PR: 101
File: docs/rfcs/runtime-rearchitecture/ja/core-runtime.md:0-0
Timestamp: 2026-03-08T05:06:48.701Z
Learning: Applies to **/*.{js,ts} and docs: In `packages/durably`, steps are created via `step.run()` and each step's success state and return value is persisted. Step output data is deleted when runs reach terminal state by default (`preserveSteps: false`). Setting `preserveSteps: true` retains step history for audit/debug purposes. The option was previously called `cleanupSteps` (inverted semantics: `cleanupSteps: true` meant delete) and was renamed to `preserveSteps` in Phase 1.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-react-router-spa/app/routes/_index/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:25.739Z
Learning: In example code under the examples/ directory, error handling is intentionally omitted to keep the code simple and focused on demonstrating the Durably API. Treat these files as illustrative, not production-ready. For real applications, ensure proper error handling and user feedback; do not copy this pattern verbatim into production code.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:36-53
Timestamp: 2026-01-02T14:03:31.683Z
Learning: In all example files under the examples directory (e.g., examples/**/*.tsx), prioritize simplicity and clarity over comprehensive error handling. Focus on demonstrating Durably API usage patterns; omit production-grade error handling in examples to keep them readable and centered on core concepts. Where appropriate, include concise comments to explain the pattern being demonstrated.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/fullstack-react-router/app/routes/_index.tsx:28-46
Timestamp: 2026-01-02T14:03:32.230Z
Learning: In the coji/durably repository's examples directory, code examples (e.g., any .tsx under examples) should prioritize simplicity and readability over comprehensive error handling to clearly demonstrate the Durably API usage patterns. Include minimal, demonstrative error handling only as necessary for clarity.

Comment on lines +213 to +223
{(run.status === 'failed' ||
run.status === 'cancelled') && (
<button
type="button"
onClick={() => handleRetrigger(run.id)}
disabled={isActioning}
className="text-xs text-green-600 hover:text-green-800 disabled:cursor-not-allowed disabled:text-gray-400"
>
Retrigger
</button>
)}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the project structure
git ls-files | head -20

Repository: coji/durably

Length of output: 640


🏁 Script executed:

# Search for documentation about retrigger
find . -type f \( -name "*.md" -o -name "*.ts" -o -name "*.tsx" \) | grep -E "(README|CLAUDE|docs)" | head -20

Repository: coji/durably

Length of output: 1100


🏁 Script executed:

# Look for the useRunActions hook definition
rg "useRunActions" -A 10 -B 2 | head -100

Repository: coji/durably

Length of output: 5141


🏁 Script executed:

# Search for retrigger function definition and documentation
rg "retrigger" -A 5 -B 2 | head -150

Repository: coji/durably

Length of output: 8650


🏁 Script executed:

# Search for API documentation specifically about retrigger
rg "retrigger" -A 10 -B 5 website/api/ | head -200

Repository: coji/durably

Length of output: 12287


🏁 Script executed:

# Look for TypeScript type definitions for retrigger
rg "retrigger" -A 5 -B 2 --type ts --type tsx | grep -E "(function|const|interface|type|status)" | head -150

Repository: coji/durably

Length of output: 83


🏁 Script executed:

# Check CLAUDE.md for any guidance about retrigger
cat CLAUDE.md

Repository: coji/durably

Length of output: 3932


🏁 Script executed:

# Search for the actual useRunActions hook implementation
fd "useRunActions" -type f --exec cat {} \;

Repository: coji/durably

Length of output: 226


🏁 Script executed:

# Check the specific file and lines mentioned in the review
sed -n '210,225p' examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx

Repository: coji/durably

Length of output: 827


🏁 Script executed:

# Also look at a few more lines for context
sed -n '200,230p' examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx

Repository: coji/durably

Length of output: 1631


Expose retrigger for completed runs too.

The API documentation explicitly states retrigger() supports completed, failed, and cancelled runs, but this guard only renders the button for failed/cancelled runs. Completed runs currently lose a supported dashboard action.

Suggested fix
-                        {(run.status === 'failed' ||
+                        {(run.status === 'completed' ||
+                          run.status === 'failed' ||
                           run.status === 'cancelled') && (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{(run.status === 'failed' ||
run.status === 'cancelled') && (
<button
type="button"
onClick={() => handleRetrigger(run.id)}
disabled={isActioning}
className="text-xs text-green-600 hover:text-green-800 disabled:cursor-not-allowed disabled:text-gray-400"
>
Retrigger
</button>
)}
{(run.status === 'completed' ||
run.status === 'failed' ||
run.status === 'cancelled') && (
<button
type="button"
onClick={() => handleRetrigger(run.id)}
disabled={isActioning}
className="text-xs text-green-600 hover:text-green-800 disabled:cursor-not-allowed disabled:text-gray-400"
>
Retrigger
</button>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines
213 - 223, The retrigger button is only rendered when run.status is 'failed' or
'cancelled', but the API supports 'completed' runs too; update the conditional
that controls rendering (the JSX block using run.status and the onClick handler
handleRetrigger) to also include 'completed' (e.g., allow run.status ===
'completed' or change to permit statuses other than 'running'/'pending' as
appropriate) so completed runs show the "Retrigger" button and remain disabled
when isActioning is true.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. This is a demo dashboard — keeping it simple.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coji, understood — keeping the demo lean makes sense!

(¬、¬)


✏️ Learnings added
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx:212-222
Timestamp: 2026-03-12T13:19:12.919Z
Learning: In `examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx`, the Retrigger button is intentionally shown only for `failed` and `cancelled` runs (not `completed`), by design to keep the demo dashboard simple. Do not suggest adding retrigger support for completed runs in this file.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/data-sync.ts:11-55
Timestamp: 2026-03-12T13:18:37.201Z
Learning: In `examples/fullstack-vercel-turso`, all example jobs (dataSyncJob, generateReportJob, importCsvJob, processImageJob) intentionally omit `preserveSteps: true` to demonstrate the default behavior where step history is deleted when runs reach a terminal state. The dashboard's View button showing empty steps for completed runs is acceptable and by design. Do not suggest adding `preserveSteps: true` to these jobs.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-react-router-spa/app/routes/_index/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:25.739Z
Learning: In example code under the examples/ directory, error handling is intentionally omitted to keep the code simple and focused on demonstrating the Durably API. Treat these files as illustrative, not production-ready. For real applications, ensure proper error handling and user feedback; do not copy this pattern verbatim into production code.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:36-53
Timestamp: 2026-01-02T14:03:31.683Z
Learning: In all example files under the examples directory (e.g., examples/**/*.tsx), prioritize simplicity and clarity over comprehensive error handling. Focus on demonstrating Durably API usage patterns; omit production-grade error handling in examples to keep them readable and centered on core concepts. Where appropriate, include concise comments to explain the pattern being demonstrated.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/fullstack-react-router/app/routes/_index.tsx:28-46
Timestamp: 2026-01-02T14:03:32.230Z
Learning: In the coji/durably repository's examples directory, code examples (e.g., any .tsx under examples) should prioritize simplicity and readability over comprehensive error handling to clearly demonstrate the Durably API usage patterns. Include minimal, demonstrative error handling only as necessary for clarity.

Comment on lines +279 to +289
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl">
<div className="p-6">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold">Run Details</h3>
<button
type="button"
onClick={() => setSelectedRun(null)}
className="text-gray-400 hover:text-gray-600"
>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add real dialog semantics to the details modal.

Right now this is only a visual overlay. Without role="dialog", aria-modal, and an accessible name on the close button, assistive-tech users do not get a proper modal context.

💡 Minimal accessibility fix
         <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
-          <div className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl">
+          <div
+            className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl"
+            role="dialog"
+            aria-modal="true"
+            aria-labelledby="run-details-title"
+          >
             <div className="p-6">
               <div className="mb-4 flex items-center justify-between">
-                <h3 className="text-lg font-semibold">Run Details</h3>
+                <h3 id="run-details-title" className="text-lg font-semibold">
+                  Run Details
+                </h3>
                 <button
                   type="button"
+                  aria-label="Close run details"
+                  autoFocus
                   onClick={() => setSelectedRun(null)}
                   className="text-gray-400 hover:text-gray-600"
                 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl">
<div className="p-6">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold">Run Details</h3>
<button
type="button"
onClick={() => setSelectedRun(null)}
className="text-gray-400 hover:text-gray-600"
>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div
className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl"
role="dialog"
aria-modal="true"
aria-labelledby="run-details-title"
>
<div className="p-6">
<div className="mb-4 flex items-center justify-between">
<h3 id="run-details-title" className="text-lg font-semibold">Run Details</h3>
<button
type="button"
aria-label="Close run details"
autoFocus
onClick={() => setSelectedRun(null)}
className="text-gray-400 hover:text-gray-600"
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines
279 - 289, The modal overlay is only visual; add proper dialog semantics by
giving the dialog container (the element with className "mx-4 max-h-[80vh]
w-full max-w-2xl ...") role="dialog" and aria-modal="true", add an id to the
<h3> (e.g., run-details-title) and reference it via aria-labelledby on the
dialog, and ensure the close button (the button that calls setSelectedRun(null))
has an accessible name (e.g., aria-label="Close run details" or aria-labelledby)
so assistive tech receives a proper modal context.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point for production code, but this is a demo app focused on showcasing Durably features, not accessibility best practices. Skipping for now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coji, understood! That's a reasonable call for a demo app. I'll keep this in mind for future reviews of this example.


✏️ Learnings added
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx:278-288
Timestamp: 2026-03-12T13:19:19.875Z
Learning: In `examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx`, the modal dialog intentionally omits accessibility attributes (`role="dialog"`, `aria-modal`, `aria-label` on close button) because this is a demo app focused on showcasing Durably features, not accessibility best practices. Do not suggest accessibility improvements for modal dialogs in this file.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/data-sync.ts:11-55
Timestamp: 2026-03-12T13:18:37.201Z
Learning: In `examples/fullstack-vercel-turso`, all example jobs (dataSyncJob, generateReportJob, importCsvJob, processImageJob) intentionally omit `preserveSteps: true` to demonstrate the default behavior where step history is deleted when runs reach a terminal state. The dashboard's View button showing empty steps for completed runs is acceptable and by design. Do not suggest adding `preserveSteps: true` to these jobs.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-react-router-spa/app/routes/_index/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:25.739Z
Learning: In example code under the examples/ directory, error handling is intentionally omitted to keep the code simple and focused on demonstrating the Durably API. Treat these files as illustrative, not production-ready. For real applications, ensure proper error handling and user feedback; do not copy this pattern verbatim into production code.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:36-53
Timestamp: 2026-01-02T14:03:31.683Z
Learning: In all example files under the examples directory (e.g., examples/**/*.tsx), prioritize simplicity and clarity over comprehensive error handling. Focus on demonstrating Durably API usage patterns; omit production-grade error handling in examples to keep them readable and centered on core concepts. Where appropriate, include concise comments to explain the pattern being demonstrated.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/fullstack-react-router/app/routes/_index.tsx:28-46
Timestamp: 2026-01-02T14:03:32.230Z
Learning: In the coji/durably repository's examples directory, code examples (e.g., any .tsx under examples) should prioritize simplicity and readability over comprehensive error handling to clearly demonstrate the Durably API usage patterns. Include minimal, demonstrative error handling only as necessary for clarity.

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 9, 2026

Deployment failed with the following error:

Hobby accounts are limited to daily cron jobs. This cron expression (* * * * *) would run more than once per day. Upgrade to the Pro plan to unlock all Cron Jobs features on Vercel.

Learn More: https://vercel.link/3Fpeeb1

Prevents Vercel from scanning sibling React Router apps in the
monorepo by specifying framework, installCommand, and buildCommand
explicitly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix buildCommand typo (fullstack-react-router → fullstack-vercel-turso)
- Remove unnecessary mkdir from buildCommand
- Fix worker endpoint to use loader (GET) for Vercel Cron compatibility
- Change cron schedule to daily (per-minute requires Pro plan)
- Add .vercelignore to exclude sibling examples from builder scan
- Add .vercel and .env*.local to gitignore
- Parallelize getRun/getSteps calls in dashboard
- Move constants outside component to avoid re-creation on render
- Update README to reflect actual behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract duplicated delay() into app/jobs/delay.ts
- Use runs.find() instead of getRun() in dashboard showDetails
- Simplify unnecessary JSX wrapper in _index.tsx

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
examples/fullstack-vercel-turso/README.md (1)

19-31: ⚠️ Potential issue | 🟡 Minor

Add a language hint to this fenced block.

This flow diagram is still unlabeled, so markdownlint MD040 will keep failing here. text is sufficient.

Proposed fix
-```
+```text
 User triggers job → POST /api/durably/trigger
                   ↓
 User subscribes   → GET /api/durably/subscribe?runId=xxx (SSE)
                   ↓
 onRequest         → durably.init() starts worker during SSE connection
                   ↓
 Worker processes  → steps stream via SSE in real-time
                   ↓
 SSE disconnects   → function terminates, worker stops
                   ↓
 Vercel Cron       → GET /api/worker processes any remaining pending jobs
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @examples/fullstack-vercel-turso/README.md around lines 19 - 31, The fenced
diagram block in README.md is missing a language hint which triggers
markdownlint MD040; update the triple-backtick opening fence for the flow
diagram (the fenced block containing "User triggers job → POST
/api/durably/trigger" through "Vercel Cron → GET /api/worker") to include the
language hint "text" (i.e., change totext) so the block is explicitly
labeled.


</details>

</blockquote></details>
<details>
<summary>examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx (2)</summary><blockquote>

`278-289`: _⚠️ Potential issue_ | _🟠 Major_

**Add real dialog semantics to the details modal.**

This is still only a visual overlay. Add `role="dialog"`, `aria-modal`, `aria-labelledby`, and an accessible name on the icon-only close button.

<details>
<summary>Minimal fix</summary>

```diff
         <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
-          <div className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl">
+          <div
+            className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl"
+            role="dialog"
+            aria-modal="true"
+            aria-labelledby="run-details-title"
+          >
             <div className="p-6">
               <div className="mb-4 flex items-center justify-between">
-                <h3 className="text-lg font-semibold">Run Details</h3>
+                <h3 id="run-details-title" className="text-lg font-semibold">
+                  Run Details
+                </h3>
                 <button
                   type="button"
+                  aria-label="Close run details"
                   onClick={() => setSelectedRun(null)}
                   className="text-gray-400 hover:text-gray-600"
                 >
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines
278 - 289, The modal overlay lacks proper ARIA semantics—add role="dialog" and
aria-modal="true" to the container div that currently holds the modal (the div
wrapping the modal content), give the title element (the h3 with text "Run
Details") a stable id (e.g., run-details-title) and set aria-labelledby on the
dialog container to that id, and make the icon-only close button accessible by
adding an aria-label (e.g., aria-label="Close run details") while keeping its
onClick that calls setSelectedRun(null).
```

</details>

---

`212-221`: _⚠️ Potential issue_ | _🟡 Minor_

**Completed runs still don’t expose `Retrigger`.**

The `retrigger()` contract includes completed runs, but this guard still hides the button for that state.

<details>
<summary>Minimal fix</summary>

```diff
-                        {(run.status === 'failed' ||
+                        {(run.status === 'completed' ||
+                          run.status === 'failed' ||
                           run.status === 'cancelled') && (
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines
212 - 221, The Retrigger button is hidden for completed runs even though
retrigger() supports them; update the render guard in the JSX so that
handleRetrigger(run.id) is shown for completed runs as well — e.g., change the
condition using run.status (currently checking 'failed' || 'cancelled') to
include 'completed' (or use a small helper like isRetriggerableStatus that
returns true for 'failed','cancelled','completed'); keep disabled tied to
isActioning and keep classNames intact.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx (1)</summary><blockquote>

`10-10`: **Align `useRuns()` with the hook’s `TInput`/`TOutput` signature.**

`useRuns` is declared as `useRuns<TInput, TOutput>()`. Passing a `TypedClientRun` union here is one level too high, so this example is demonstrating a shape that does not match the client API.

<details>
<summary>Minimal fix</summary>

```diff
-import type { ClientRun, StepRecord, TypedClientRun } from '@coji/durably-react'
+import type { ClientRun, StepRecord } from '@coji/durably-react'

-/** Union type for all job runs in this dashboard */
-type DashboardRun =
-  | TypedClientRun<DataSyncInput, DataSyncOutput>
-  | TypedClientRun<GenerateReportInput, GenerateReportOutput>
-  | TypedClientRun<ImportCsvInput, ImportCsvOutput>
-  | TypedClientRun<ProcessImageInput, ProcessImageOutput>
+type DashboardInput =
+  | DataSyncInput
+  | GenerateReportInput
+  | ImportCsvInput
+  | ProcessImageInput
+
+type DashboardOutput =
+  | DataSyncOutput
+  | GenerateReportOutput
+  | ImportCsvOutput
+  | ProcessImageOutput

-    durably.useRuns<DashboardRun>({
+    durably.useRuns<DashboardInput, DashboardOutput>({
```
</details>



Also applies to: 24-29, 59-62

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` at line 10,
The hook call uses the union type TypedClientRun as the generic but useRuns is
declared as useRuns<TInput, TOutput>(), so replace the single TypedClientRun
generic with the proper two generics (e.g., useRuns<ClientRun, StepRecord>()),
updating the useRuns<TypedClientRun> occurrences to useRuns<ClientRun,
StepRecord> (or other concrete TInput/TOutput types) so the generic parameters
match the hook signature; search for useRuns< and change all instances that
currently pass TypedClientRun to the two-type form.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx:

  • Around line 278-289: The modal overlay lacks proper ARIA semantics—add
    role="dialog" and aria-modal="true" to the container div that currently holds
    the modal (the div wrapping the modal content), give the title element (the h3
    with text "Run Details") a stable id (e.g., run-details-title) and set
    aria-labelledby on the dialog container to that id, and make the icon-only close
    button accessible by adding an aria-label (e.g., aria-label="Close run details")
    while keeping its onClick that calls setSelectedRun(null).
  • Around line 212-221: The Retrigger button is hidden for completed runs even
    though retrigger() supports them; update the render guard in the JSX so that
    handleRetrigger(run.id) is shown for completed runs as well — e.g., change the
    condition using run.status (currently checking 'failed' || 'cancelled') to
    include 'completed' (or use a small helper like isRetriggerableStatus that
    returns true for 'failed','cancelled','completed'); keep disabled tied to
    isActioning and keep classNames intact.

In @examples/fullstack-vercel-turso/README.md:

  • Around line 19-31: The fenced diagram block in README.md is missing a language
    hint which triggers markdownlint MD040; update the triple-backtick opening fence
    for the flow diagram (the fenced block containing "User triggers job → POST
    /api/durably/trigger" through "Vercel Cron → GET /api/worker") to include the
    language hint "text" (i.e., change totext) so the block is explicitly
    labeled.

Nitpick comments:
In @examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx:

  • Line 10: The hook call uses the union type TypedClientRun as the generic but
    useRuns is declared as useRuns<TInput, TOutput>(), so replace the single
    TypedClientRun generic with the proper two generics (e.g., useRuns<ClientRun,
    StepRecord>()), updating the useRuns occurrences to
    useRuns<ClientRun, StepRecord> (or other concrete TInput/TOutput types) so the
    generic parameters match the hook signature; search for useRuns< and change all
    instances that currently pass TypedClientRun to the two-type form.

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `20a535b0-7e15-4993-9199-c498769c764c`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9183553c3ac80f2d583df877484fc9c35fbb0ddf and 3956cb3430871adbddd448412c690227942b0eb5.

</details>

<details>
<summary>📒 Files selected for processing (8)</summary>

* `.gitignore`
* `CLAUDE.md`
* `examples/fullstack-vercel-turso/.gitignore`
* `examples/fullstack-vercel-turso/.vercelignore`
* `examples/fullstack-vercel-turso/README.md`
* `examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx`
* `examples/fullstack-vercel-turso/app/routes/api.worker.ts`
* `examples/fullstack-vercel-turso/vercel.json`

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* examples/fullstack-vercel-turso/app/routes/api.worker.ts
* examples/fullstack-vercel-turso/.gitignore

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (6)
examples/fullstack-vercel-turso/app/jobs/generate-report.ts (2)

200-206: ⚠️ Potential issue | 🟡 Minor

Encode department before interpolating into the URL.

Special characters in department (spaces, /, ?, #) will break the URL structure.

💡 Fix
     const reportUrl = await step.run('upload-report', async () => {
       step.progress(20, T, 'Uploading report...')
       await delay(4000)
-      const url = `https://reports.example.com/${input.department}/${input.reportType}-${Date.now()}.pdf`
+      const departmentPath = encodeURIComponent(input.department)
+      const url = `https://reports.example.com/${departmentPath}/${input.reportType}-${Date.now()}.pdf`
       step.log.info(`Report uploaded: ${url}`)
       return url
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts` around lines 200
- 206, The URL construction in the upload-report step interpolates
input.department directly which can break URLs for values with spaces or
reserved characters; update the anonymous function passed to step.run (the
upload-report block that returns url and logs via step.log.info) to URL-encode
input.department (e.g., use encodeURIComponent on input.department) before
building the url string so the generated reportUrl is safe and valid.

216-220: ⚠️ Potential issue | 🟡 Minor

Wrap generatedAt in a step to persist the timestamp.

If the job resumes after the last persisted step, generatedAt will reflect the resume time rather than actual completion time.

💡 Fix
+    const generatedAt = await step.run('finalize-report', async () => {
+      return new Date().toISOString()
+    })
+
     return {
       reportUrl,
       totalRecords,
-      generatedAt: new Date().toISOString(),
+      generatedAt,
     }

Based on learnings: "step execution uses step.run() and step output data is persisted."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts` around lines 216
- 220, The returned generatedAt timestamp is computed at job return time and can
be wrong if the job resumes; move the timestamp creation into a persisted step
so it is saved immediately via step.run(). In the generate-report job (around
the final return), add a step (e.g., persistGeneratedAt or stepGeneratedAt) and
call step.run() with an output object containing generatedAt: new
Date().toISOString(), then include that persisted value in the final returned
object (reportUrl, totalRecords, generatedAt) so the completion time is reliably
persisted.
examples/fullstack-vercel-turso/app/jobs/data-sync.ts (1)

11-14: ⚠️ Potential issue | 🟠 Major

Add preserveSteps: true to retain step history for the dashboard.

The dashboard displays step history for runs, but without preserveSteps: true, steps are deleted when runs reach terminal state. Completed runs will show an empty steps list.

💡 Fix
 export const dataSyncJob = defineJob({
   name: 'data-sync',
   input: z.object({ userId: z.string() }),
   output: z.object({ synced: z.number(), failed: z.number() }),
+  preserveSteps: true,
   run: async (step, input) => {

Based on learnings: "By default, preserveSteps is false (step history is deleted when runs reach a terminal state). If audit/debug history is required, ensure preserveSteps is set to true."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/jobs/data-sync.ts` around lines 11 - 14,
The job definition for dataSyncJob uses defineJob but doesn't set preserveSteps,
so step history is removed at terminal state; update the defineJob call for
dataSyncJob to include preserveSteps: true in its configuration (i.e., add
preserveSteps: true to the object passed to defineJob) so the dashboard retains
step history for completed runs.
examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx (3)

91-98: ⚠️ Potential issue | 🟡 Minor

Reset steps before switching to another run to avoid stale data flash.

The current implementation sets selectedRun before getSteps() completes, causing the previous run's steps to briefly display under the newly selected run.

💡 Minimal fix
   const showDetails = async (runId: string) => {
+    setSteps([])
     const run = runs.find((r) => r.id === runId)
     if (run) {
       setSelectedRun(run)
       const stepsData = await getSteps(runId)
       setSteps(stepsData)
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines
91 - 98, In showDetails, avoid flashing stale steps by clearing the steps state
before changing selectedRun and awaiting getSteps; call setSteps([]) (or null)
immediately when a matching run is found, then setSelectedRun(run), await
getSteps(runId), and finally setSteps(stepsData) so the UI doesn't render the
previous run's steps while loading; reference showDetails, getSteps,
setSelectedRun, setSteps and runs to locate the change.

212-222: ⚠️ Potential issue | 🟡 Minor

Expose retrigger for completed runs too.

The API supports retriggering completed, failed, and cancelled runs, but this condition excludes completed runs from showing the Retrigger button.

💡 Suggested fix
-                        {(run.status === 'failed' ||
+                        {(run.status === 'completed' ||
+                          run.status === 'failed' ||
                           run.status === 'cancelled') && (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines
212 - 222, The UI condition currently only shows the Retrigger button when
run.status is 'failed' or 'cancelled'; update the conditional in the component
rendering the button (where run.status is checked and handleRetrigger is used)
to also include 'completed' so completed runs can be retriggered; ensure the
onClick still calls handleRetrigger(run.id) and the disabled/isActioning logic
and button classes remain unchanged.

278-290: ⚠️ Potential issue | 🟠 Major

Add dialog semantics to the modal for accessibility.

The modal overlay lacks proper ARIA attributes. Screen reader and keyboard users won't get proper modal context.

💡 Accessibility fix
         <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
-          <div className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl">
+          <div
+            className="mx-4 max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-white shadow-xl"
+            role="dialog"
+            aria-modal="true"
+            aria-labelledby="run-details-title"
+          >
             <div className="p-6">
               <div className="mb-4 flex items-center justify-between">
-                <h3 className="text-lg font-semibold">Run Details</h3>
+                <h3 id="run-details-title" className="text-lg font-semibold">Run Details</h3>
                 <button
                   type="button"
+                  aria-label="Close run details"
                   onClick={() => setSelectedRun(null)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines
278 - 290, The modal overlay markup needs proper ARIA dialog semantics: update
the outer modal container (the div that currently wraps the modal content — the
element rendered when selectedRun is set) to include role="dialog" and
aria-modal="true", add an id on the heading (the h3 "Run Details") and reference
it via aria-labelledby on the dialog, and ensure the close button (onClick
setSelectedRun(null)) has an accessible name (it already has "✕" — add
aria-label="Close" or visually-hidden text). Optionally ensure focus is moved
into the dialog when opened and returned when closed (use the same container
where you add role="dialog" to implement focus management).
🧹 Nitpick comments (1)
examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx (1)

362-380: Consider using a more stable key for steps list.

Using s.name as the key assumes step names are unique within a run. If duplicate step names exist, React may render incorrectly. Consider using a composite key or index if the API doesn't guarantee uniqueness.

💡 Safer key approach
                     {steps.map((s) => (
                        <li
-                          key={s.name}
+                          key={`${s.name}-${s.status}`}
                          className="flex items-center justify-between p-2"
                        >

Or if s.id is available, use that instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx` around lines
362 - 380, The list mapping for steps uses s.name as the React key which may not
be unique; update the key in the steps.map(...) JSX to a stable unique
identifier (prefer s.id if present, e.g., use s.id) or fall back to a composite
key like `${runId}-${s.name}` or `${runId}-${index}` when s.id is missing;
locate the map over steps (steps.map((s) => ...)) and replace the key={s.name}
with the chosen stable key to avoid duplicate-key rendering issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts`:
- Around line 36-42: The generateReportJob defined via defineJob should retain
step history for completed runs; update the job definition (generateReportJob)
to include preserveSteps: true in its options so the dashboard can display steps
for terminal runs—i.e., add the preserveSteps: true property alongside name,
input, and output in the defineJob call.

In `@examples/fullstack-vercel-turso/app/jobs/process-image.ts`:
- Around line 11-14: The job definition for processImageJob (created via
defineJob) is missing preserveSteps: true, so step history is deleted at
terminal state; update the defineJob call for processImageJob to include the
option preserveSteps: true (e.g., add preserveSteps: true to the job config
alongside name, input, and output) so completed runs retain their step history
in the dashboard.

---

Duplicate comments:
In `@examples/fullstack-vercel-turso/app/jobs/data-sync.ts`:
- Around line 11-14: The job definition for dataSyncJob uses defineJob but
doesn't set preserveSteps, so step history is removed at terminal state; update
the defineJob call for dataSyncJob to include preserveSteps: true in its
configuration (i.e., add preserveSteps: true to the object passed to defineJob)
so the dashboard retains step history for completed runs.

In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts`:
- Around line 200-206: The URL construction in the upload-report step
interpolates input.department directly which can break URLs for values with
spaces or reserved characters; update the anonymous function passed to step.run
(the upload-report block that returns url and logs via step.log.info) to
URL-encode input.department (e.g., use encodeURIComponent on input.department)
before building the url string so the generated reportUrl is safe and valid.
- Around line 216-220: The returned generatedAt timestamp is computed at job
return time and can be wrong if the job resumes; move the timestamp creation
into a persisted step so it is saved immediately via step.run(). In the
generate-report job (around the final return), add a step (e.g.,
persistGeneratedAt or stepGeneratedAt) and call step.run() with an output object
containing generatedAt: new Date().toISOString(), then include that persisted
value in the final returned object (reportUrl, totalRecords, generatedAt) so the
completion time is reliably persisted.

In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx`:
- Around line 91-98: In showDetails, avoid flashing stale steps by clearing the
steps state before changing selectedRun and awaiting getSteps; call setSteps([])
(or null) immediately when a matching run is found, then setSelectedRun(run),
await getSteps(runId), and finally setSteps(stepsData) so the UI doesn't render
the previous run's steps while loading; reference showDetails, getSteps,
setSelectedRun, setSteps and runs to locate the change.
- Around line 212-222: The UI condition currently only shows the Retrigger
button when run.status is 'failed' or 'cancelled'; update the conditional in the
component rendering the button (where run.status is checked and handleRetrigger
is used) to also include 'completed' so completed runs can be retriggered;
ensure the onClick still calls handleRetrigger(run.id) and the
disabled/isActioning logic and button classes remain unchanged.
- Around line 278-290: The modal overlay markup needs proper ARIA dialog
semantics: update the outer modal container (the div that currently wraps the
modal content — the element rendered when selectedRun is set) to include
role="dialog" and aria-modal="true", add an id on the heading (the h3 "Run
Details") and reference it via aria-labelledby on the dialog, and ensure the
close button (onClick setSelectedRun(null)) has an accessible name (it already
has "✕" — add aria-label="Close" or visually-hidden text). Optionally ensure
focus is moved into the dialog when opened and returned when closed (use the
same container where you add role="dialog" to implement focus management).

---

Nitpick comments:
In `@examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx`:
- Around line 362-380: The list mapping for steps uses s.name as the React key
which may not be unique; update the key in the steps.map(...) JSX to a stable
unique identifier (prefer s.id if present, e.g., use s.id) or fall back to a
composite key like `${runId}-${s.name}` or `${runId}-${index}` when s.id is
missing; locate the map over steps (steps.map((s) => ...)) and replace the
key={s.name} with the chosen stable key to avoid duplicate-key rendering issues.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0ee173ac-3291-4fb1-82cb-c2b27cf9f07c

📥 Commits

Reviewing files that changed from the base of the PR and between 3956cb3 and e89a70e.

📒 Files selected for processing (7)
  • examples/fullstack-vercel-turso/app/jobs/data-sync.ts
  • examples/fullstack-vercel-turso/app/jobs/delay.ts
  • examples/fullstack-vercel-turso/app/jobs/generate-report.ts
  • examples/fullstack-vercel-turso/app/jobs/import-csv.ts
  • examples/fullstack-vercel-turso/app/jobs/process-image.ts
  • examples/fullstack-vercel-turso/app/routes/_index.tsx
  • examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/fullstack-vercel-turso/app/jobs/import-csv.ts

Comment on lines +36 to +42
export const generateReportJob = defineJob({
name: 'generate-report',
input: z.object({
reportType: z.enum(['daily', 'weekly', 'monthly']),
department: z.string(),
}),
output: outputSchema,
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add preserveSteps: true to retain step history for the dashboard.

Like the other jobs, this long-running report job should preserve step history so the dashboard can display steps for completed runs.

💡 Fix
 export const generateReportJob = defineJob({
   name: 'generate-report',
   input: z.object({
     reportType: z.enum(['daily', 'weekly', 'monthly']),
     department: z.string(),
   }),
   output: outputSchema,
+  preserveSteps: true,
   run: async (step, input) => {

Based on learnings: "By default, preserveSteps is false (step history is deleted when runs reach a terminal state)."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const generateReportJob = defineJob({
name: 'generate-report',
input: z.object({
reportType: z.enum(['daily', 'weekly', 'monthly']),
department: z.string(),
}),
output: outputSchema,
export const generateReportJob = defineJob({
name: 'generate-report',
input: z.object({
reportType: z.enum(['daily', 'weekly', 'monthly']),
department: z.string(),
}),
output: outputSchema,
preserveSteps: true,
run: async (step, input) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/jobs/generate-report.ts` around lines 36
- 42, The generateReportJob defined via defineJob should retain step history for
completed runs; update the job definition (generateReportJob) to include
preserveSteps: true in its options so the dashboard can display steps for
terminal runs—i.e., add the preserveSteps: true property alongside name, input,
and output in the defineJob call.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentionally not adding preserveSteps — we want to demonstrate the default cleanup behavior. Steps are visible during execution via SSE, and the View detail modal will show empty steps for completed runs, which is the expected default.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coji, understood — this is already captured in my knowledge base. The intentional omission of preserveSteps across all jobs in this example is by design to showcase the default cleanup behavior, where steps are visible during execution via SSE but cleared once the run reaches a terminal state. I won't flag this again. Apologies for the noise!


🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/data-sync.ts:11-55
Timestamp: 2026-03-12T13:18:37.201Z
Learning: In `examples/fullstack-vercel-turso`, all example jobs (dataSyncJob, generateReportJob, importCsvJob, processImageJob) intentionally omit `preserveSteps: true` to demonstrate the default behavior where step history is deleted when runs reach a terminal state. The dashboard's View button showing empty steps for completed runs is acceptable and by design. Do not suggest adding `preserveSteps: true` to these jobs.

Learnt from: coji
Repo: coji/durably PR: 101
File: docs/rfcs/runtime-rearchitecture/ja/core-runtime.md:0-0
Timestamp: 2026-03-08T05:06:39.050Z
Learning: Reviewers should verify that step execution uses step.run() and that step output data is persisted. By default, preserveSteps is false (step history is deleted when runs reach a terminal state). If audit/debug history is required, ensure preserveSteps is set to true. Be aware that cleanupSteps used to exist with inverted semantics (cleanupSteps: true meant delete), and this was renamed to preserveSteps in Phase 1. Apply this guidance to all JS/TS code and docs that reference step persistence.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.

Comment on lines +11 to +14
export const processImageJob = defineJob({
name: 'process-image',
input: z.object({ filename: z.string(), width: z.number() }),
output: z.object({ url: z.string(), size: z.number() }),
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add preserveSteps: true to retain step history for the dashboard.

Consistent with the other jobs, this should preserve step history so completed runs display their steps in the dashboard.

💡 Fix
 export const processImageJob = defineJob({
   name: 'process-image',
   input: z.object({ filename: z.string(), width: z.number() }),
   output: z.object({ url: z.string(), size: z.number() }),
+  preserveSteps: true,
   run: async (step, input) => {

Based on learnings: "By default, preserveSteps is false (step history is deleted when runs reach a terminal state)."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const processImageJob = defineJob({
name: 'process-image',
input: z.object({ filename: z.string(), width: z.number() }),
output: z.object({ url: z.string(), size: z.number() }),
export const processImageJob = defineJob({
name: 'process-image',
input: z.object({ filename: z.string(), width: z.number() }),
output: z.object({ url: z.string(), size: z.number() }),
preserveSteps: true,
run: async (step, input) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/jobs/process-image.ts` around lines 11 -
14, The job definition for processImageJob (created via defineJob) is missing
preserveSteps: true, so step history is deleted at terminal state; update the
defineJob call for processImageJob to include the option preserveSteps: true
(e.g., add preserveSteps: true to the job config alongside name, input, and
output) so completed runs retain their step history in the dashboard.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above — intentionally using the default (no step preservation).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rate Limit Exceeded

@coji have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 36 seconds before sending another message.

- Add await durably.init() in _index action to ensure DB tables exist on cold start
- Remove orphaned .dockerignore (no Dockerfile exists)
- Remove duplicate type exports from individual job files (single source in index.ts)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pnpm --filter doesn't build workspace dependencies (@coji/durably,
@coji/durably-react). Use turbo which respects the dependency graph
and ensures dist/ exists even with warm Vercel build cache.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add preserveSteps: true to createDurably so dashboard can show step history
- Remove PII (name, email, amount) from import-csv job logs, use row ID instead
- Add language hint to README fenced code block

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coji coji merged commit 6ef0b14 into main Mar 12, 2026
5 checks passed
@coji coji deleted the feat/example-fullstack-vercel-turso branch March 12, 2026 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant