feat: add Kubernetes-style labels for run filtering#31
Conversation
Add `labels: Record<string, string>` to runs for multi-tenant filtering. Labels are set at trigger time, stored as JSON in SQLite, and included in all run-scoped events. Filtering uses AND semantics via json_extract. - DB migration v2: add labels column to durably_runs - Storage: labels on Run, CreateRunInput, RunFilter with json_extract filtering - Events: labels on all run/step event types - Job handle: labels in TriggerOptions, passed through trigger/batchTrigger - Worker/Durably: labels in all event emissions - HTTP server: label.* query params for runs and SSE filtering - React hooks: labels option for useRuns (browser + client modes) - React hooks: stabilize labels object ref with useMemo to prevent infinite re-render loops when callers pass inline objects Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- packages/durably/docs/llms.md: labels in trigger, query, types - website/api/: labels in index, create-durably, events, http-handler - website/api/durably-react/: labels in browser.md and client.md - website/guide/concepts.md: new Labels section - website/public/llms.txt: regenerated Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove openspec/ directory and all references (AGENTS.md, CLAUDE.md, settings, release-check skill) - Add .claude/skills/doc-check/ for documentation update checklists - Remove stale docs/spec.md references from CLAUDE.md and release-check Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add labels to browser example trigger calls (Image Processing only, keeping Data Sync simple for contrast). Display labels in all three dashboard components with table column and modal section. Add labels field to ClientRun and RunRecord types. Update all docs to show simple labels example before multi-tenancy pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (12)
📝 WalkthroughWalkthroughAdds comprehensive label support across server, storage, events, worker, client hooks, docs, and examples; and removes multiple OpenSpec documentation/spec files while adding a documentation checklist SKILL. Labels are persisted on runs, propagated in events, and usable as filters in getRuns/useRuns/SSE. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Server
participant DB as Database
participant Worker
Client->>Server: POST /trigger {payload, labels}
Server->>DB: storage.createRun(payload, labels JSON)
DB-->>Server: Run created (with labels)
Server->>Server: emit run:trigger (labels)
Worker->>DB: claim/poll runs
Worker->>DB: update run status/start
Worker->>Server: emit run:start/run:progress/run:complete (include labels)
Server->>Client: SSE events (filtered by labels)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Restrict label keys to alphanumeric characters, dashes, underscores, dots, and slashes (Kubernetes-style). Validation runs on createRun, batchCreateRuns, and getRuns filter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (6)
examples/fullstack-react-router/app/routes/_index.tsx (1)
42-46: Consider adding labels to thedataSynctrigger for consistency.The
processImagetrigger includes labels withsource: 'server', butdataSyncdoes not. If this is intentional to keep the example focused on demonstrating labels in one place, that's fine. However, for completeness and to better demonstrate multi-tenant filtering across all job types, you might consider adding labels here as well.💡 Optional: Add labels to dataSync trigger
if (intent === 'sync') { const userId = formData.get('userId') as string - const run = await durably.jobs.dataSync.trigger({ userId }) + const run = await durably.jobs.dataSync.trigger( + { userId }, + { labels: { source: 'server' } }, + ) return { intent: 'sync', runId: run.id } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/fullstack-react-router/app/routes/_index.tsx` around lines 42 - 46, The dataSync trigger call lacks labels; update the block handling intent === 'sync' to pass a labels object similar to processImage (e.g., include source: 'server' and any tenant/user labels) when calling durably.jobs.dataSync.trigger, so the call durably.jobs.dataSync.trigger({ userId }) becomes durably.jobs.dataSync.trigger({ userId, labels: { source: 'server', /* optional tenant/user */ } }) to enable consistent multi-tenant filtering and parity with processImage.examples/fullstack-react-router/app/routes/_index/dashboard.tsx (1)
156-170: Consider deduplicating label-chip JSX used in table and modal.Both blocks implement the same map/render pattern; extracting a tiny helper/component will reduce drift in future UI tweaks.
♻️ Proposed refactor
export function Dashboard() { + const renderLabels = ( + labels: Record<string, string>, + { + showEmpty = true, + chipClassName = 'inline-block rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700', + }: { showEmpty?: boolean; chipClassName?: string } = {} + ) => { + const entries = Object.entries(labels) + if (entries.length === 0) return showEmpty ? <span className="text-gray-400">-</span> : null + return ( + <div className="flex flex-wrap gap-1"> + {entries.map(([k, v]) => ( + <span key={k} className={chipClassName}> + {k}={v} + </span> + ))} + </div> + ) + } ... - <td className="px-2 py-2"> - {Object.keys(run.labels).length > 0 ? ( - <div className="flex flex-wrap gap-1"> - {Object.entries(run.labels).map(([k, v]) => ( - <span - key={k} - className="inline-block rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700" - > - {k}={v} - </span> - ))} - </div> - ) : ( - <span className="text-gray-400">-</span> - )} - </td> + <td className="px-2 py-2">{renderLabels(run.labels)}</td> ... - {Object.keys(selectedRun.labels).length > 0 && ( + {Object.keys(selectedRun.labels).length > 0 && ( <div> <span className="font-medium text-gray-600">Labels:</span> - <div className="mt-1 flex flex-wrap gap-1"> - {Object.entries(selectedRun.labels).map(([k, v]) => ( - <span - key={k} - className="inline-block rounded bg-purple-100 px-2 py-0.5 text-xs text-purple-700" - > - {k}={v} - </span> - ))} - </div> + <div className="mt-1"> + {renderLabels(selectedRun.labels, { + showEmpty: false, + chipClassName: + 'inline-block rounded bg-purple-100 px-2 py-0.5 text-xs text-purple-700', + })} + </div> </div> )}Also applies to: 312-325
CLAUDE.md (1)
63-65: Adddoc-checkto the Skills index for discoverability.Line 65 only references
release-check, but this PR introduces.claude/skills/doc-check/. Listing both keeps the entrypoint complete.Suggested patch
## Skills - **release-check** - Pre-release integrity check for API changes and spec updates (`.claude/skills/release-check/`) +- **doc-check** - Documentation update checklist after API changes (`.claude/skills/doc-check/`)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLAUDE.md` around lines 63 - 65, The Skills index currently lists only "release-check"; update the Skills section in CLAUDE.md to include both entries by adding a bullet for **doc-check** (pointing to `.claude/skills/doc-check/`) alongside **release-check** so the entrypoint is complete and discoverable.packages/durably/tests/shared/events.shared.ts (1)
31-38: Assertlabelsin listener payload expectation.Since labels are now part of the event contract, include them in this assertion to prevent silent regressions.
♻️ Proposed test assertion update
expect(listener).toHaveBeenCalledWith( expect.objectContaining({ type: 'run:start', runId: 'run_1', jobName: 'test-job', payload: { foo: 'bar' }, + labels: {}, }), )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/durably/tests/shared/events.shared.ts` around lines 31 - 38, The test assertion for the listener payload is missing the new labels field; update the expectation for the listener (the expect(listener).toHaveBeenCalledWith(expect.objectContaining({...})) block that checks the 'run:start' event/runId/jobName/payload to also assert labels is present and has the expected value (e.g., include "labels: { ... }" inside the objectContaining), so the test validates the full event contract including labels.packages/durably/src/migrations.ts (1)
113-123: Consider exporting a “latest schema version” constant from migrations.Tests currently hardcode migration count/version; exposing a single source of truth here would reduce test churn when new migrations are added.
♻️ Proposed maintainability tweak
interface Migration { version: number up: (db: Kysely<Database>) => Promise<void> } const migrations: Migration[] = [ // ... ] + +export const LATEST_SCHEMA_VERSION = + migrations[migrations.length - 1]?.version ?? 0🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/durably/src/migrations.ts` around lines 113 - 123, The migrations module defines the migrations array but lacks a single source-of-truth for the latest schema version; add and export a constant (e.g., LATEST_MIGRATION_VERSION or LATEST_SCHEMA_VERSION) computed from the migrations array (like using migrations[migrations.length - 1].version or an explicit integer) so tests can import that constant instead of hardcoding a number, and update tests to import this new exported symbol from the migrations module.website/api/http-handler.md (1)
142-143: Consider documenting multi-label matching semantics (AND).Optional: add one short note that multiple
label.*query params must all match, to make filtering behavior explicit.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/api/http-handler.md` around lines 142 - 143, Add a short explicit note to the GET /api/durably/runs endpoint documentation that multiple label.* query parameters are combined with AND semantics (i.e., all provided label filters must match for a run to be returned); update the example/query description around the GET /api/durably/runs?jobName=... line and clarify that label.organizationId=org_123&label.teamId=team_456 means both labels must match.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/durably/docs/llms.md`:
- Around line 316-319: Update the fenced code block that currently contains the
GET examples so it includes a language identifier (use "http") after the opening
backticks; locate the fenced block with the two lines "GET
/runs?label.organizationId=org_123" and "GET
/runs/subscribe?label.organizationId=org_123&label.env=prod" in llms.md and
change the opening ``` to ```http to satisfy markdownlint MD040.
In `@packages/durably/src/storage.ts`:
- Around line 357-364: The JSON path built in the labels filter loop (where you
iterate over filter?.labels and call query.where with
sql`json_extract(durably_runs.labels, ${`$.${key}`})`) fails for keys containing
dots; change the JSON path construction to use bracket notation (e.g., $['key'])
instead of $.key so dotted keys are treated as a single identifier, and ensure
any single quotes inside key are escaped/encoded before embedding in the path;
update the loop that constructs the json_extract argument (the code that reads
filter?.labels and calls query.where) to produce
json_extract(durably_runs.labels, $['...']) with proper escaping.
In `@website/guide/concepts.md`:
- Around line 186-188: The fenced code block showing the HTTP request lacks a
language identifier (triggers markdownlint MD040); update the fence around the
snippet "GET /runs/subscribe?label.organizationId=org_123" to include a language
tag (e.g., add "http" after the opening backticks) so the block becomes a fenced
code block with a language identifier.
---
Nitpick comments:
In `@CLAUDE.md`:
- Around line 63-65: The Skills index currently lists only "release-check";
update the Skills section in CLAUDE.md to include both entries by adding a
bullet for **doc-check** (pointing to `.claude/skills/doc-check/`) alongside
**release-check** so the entrypoint is complete and discoverable.
In `@examples/fullstack-react-router/app/routes/_index.tsx`:
- Around line 42-46: The dataSync trigger call lacks labels; update the block
handling intent === 'sync' to pass a labels object similar to processImage
(e.g., include source: 'server' and any tenant/user labels) when calling
durably.jobs.dataSync.trigger, so the call durably.jobs.dataSync.trigger({
userId }) becomes durably.jobs.dataSync.trigger({ userId, labels: { source:
'server', /* optional tenant/user */ } }) to enable consistent multi-tenant
filtering and parity with processImage.
In `@packages/durably/src/migrations.ts`:
- Around line 113-123: The migrations module defines the migrations array but
lacks a single source-of-truth for the latest schema version; add and export a
constant (e.g., LATEST_MIGRATION_VERSION or LATEST_SCHEMA_VERSION) computed from
the migrations array (like using migrations[migrations.length - 1].version or an
explicit integer) so tests can import that constant instead of hardcoding a
number, and update tests to import this new exported symbol from the migrations
module.
In `@packages/durably/tests/shared/events.shared.ts`:
- Around line 31-38: The test assertion for the listener payload is missing the
new labels field; update the expectation for the listener (the
expect(listener).toHaveBeenCalledWith(expect.objectContaining({...})) block that
checks the 'run:start' event/runId/jobName/payload to also assert labels is
present and has the expected value (e.g., include "labels: { ... }" inside the
objectContaining), so the test validates the full event contract including
labels.
In `@website/api/http-handler.md`:
- Around line 142-143: Add a short explicit note to the GET /api/durably/runs
endpoint documentation that multiple label.* query parameters are combined with
AND semantics (i.e., all provided label filters must match for a run to be
returned); update the example/query description around the GET
/api/durably/runs?jobName=... line and clarify that
label.organizationId=org_123&label.teamId=team_456 means both labels must match.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 79007e2f-08cf-4d14-9e38-ce9be6fa413d
📒 Files selected for processing (55)
.claude/skills/doc-check/SKILL.md.claude/skills/release-check/SKILL.mdAGENTS.mdCLAUDE.mdexamples/browser-react-router-spa/app/routes/_index.tsxexamples/browser-react-router-spa/app/routes/_index/dashboard.tsxexamples/browser-vite-react/src/App.tsxexamples/browser-vite-react/src/components/dashboard.tsxexamples/fullstack-react-router/app/routes/_index.tsxexamples/fullstack-react-router/app/routes/_index/dashboard.tsxopenspec/AGENTS.mdopenspec/changes/add-human-in-the-loop/proposal.mdopenspec/changes/add-human-in-the-loop/specs/core/spec.mdopenspec/changes/add-human-in-the-loop/specs/react/spec.mdopenspec/changes/add-human-in-the-loop/tasks.mdopenspec/changes/add-job-versioning/proposal.mdopenspec/changes/add-job-versioning/specs/core/spec.mdopenspec/changes/add-job-versioning/tasks.mdopenspec/changes/add-postgresql/proposal.mdopenspec/changes/add-postgresql/specs/core/spec.mdopenspec/changes/add-postgresql/tasks.mdopenspec/changes/add-streaming-v2/proposal.mdopenspec/changes/add-streaming-v2/specs/core/spec.mdopenspec/changes/add-streaming-v2/tasks.mdopenspec/project.mdopenspec/specs/core/spec.mdopenspec/specs/react/spec.mdpackages/durably-react/docs/llms.mdpackages/durably-react/src/client/use-run-actions.tspackages/durably-react/src/client/use-runs.tspackages/durably-react/src/hooks/use-runs.tspackages/durably-react/src/types.tspackages/durably-react/tests/client/use-runs.test.tsxpackages/durably/docs/llms.mdpackages/durably/src/context.tspackages/durably/src/durably.tspackages/durably/src/events.tspackages/durably/src/job.tspackages/durably/src/migrations.tspackages/durably/src/schema.tspackages/durably/src/server.tspackages/durably/src/storage.tspackages/durably/src/worker.tspackages/durably/tests/shared/events.shared.tspackages/durably/tests/shared/migrate.shared.tspackages/durably/tests/shared/storage.shared.tswebsite/api/create-durably.mdwebsite/api/durably-react/browser.mdwebsite/api/durably-react/client.mdwebsite/api/durably-react/types.mdwebsite/api/events.mdwebsite/api/http-handler.mdwebsite/api/index.mdwebsite/guide/concepts.mdwebsite/public/llms.txt
💤 Files with no reviewable changes (18)
- openspec/changes/add-postgresql/proposal.md
- AGENTS.md
- openspec/changes/add-streaming-v2/proposal.md
- openspec/changes/add-postgresql/tasks.md
- openspec/changes/add-job-versioning/tasks.md
- openspec/changes/add-postgresql/specs/core/spec.md
- openspec/changes/add-streaming-v2/specs/core/spec.md
- openspec/changes/add-job-versioning/specs/core/spec.md
- openspec/changes/add-streaming-v2/tasks.md
- openspec/changes/add-human-in-the-loop/specs/core/spec.md
- openspec/specs/react/spec.md
- openspec/changes/add-human-in-the-loop/specs/react/spec.md
- openspec/changes/add-human-in-the-loop/tasks.md
- openspec/AGENTS.md
- openspec/project.md
- openspec/specs/core/spec.md
- openspec/changes/add-human-in-the-loop/proposal.md
- openspec/changes/add-job-versioning/proposal.md
There was a problem hiding this comment.
♻️ Duplicate comments (1)
packages/durably/src/storage.ts (1)
380-385:⚠️ Potential issue | 🟠 MajorLabel filtering path breaks for dotted keys (still unresolved).
At Line 384,
$.${key}treats.as path traversal, so valid keys likeapp.kubernetes.io/namewon’t match as a single label key. Build a quoted JSON path segment instead.🔧 Suggested fix
if (filter?.labels) { validateLabels(filter.labels) for (const [key, value] of Object.entries(filter.labels)) { + const escapedKey = key.replaceAll('\\', '\\\\').replaceAll('"', '\\"') query = query.where( - sql`json_extract(durably_runs.labels, ${`$.${key}`})`, + sql`json_extract(durably_runs.labels, ${`$."${escapedKey}"`})`, '=', value, ) } }#!/bin/bash python - <<'PY' import sqlite3, json conn = sqlite3.connect(":memory:") cur = conn.cursor() cur.execute("create table runs(labels text not null)") cur.execute( "insert into runs(labels) values (?)", (json.dumps({"organizationId":"org_1","app.kubernetes.io/name":"img-proc"}),), ) paths = ["$.organizationId", "$.app.kubernetes.io/name", '$."app.kubernetes.io/name"'] for p in paths: cur.execute("select json_extract(labels, ?) from runs", (p,)) print(p, "=>", cur.fetchone()[0]) PY🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/durably/src/storage.ts` around lines 380 - 385, The JSON path built for json_extract in the labels filtering loop treats dots as path separators and breaks dotted keys; update the loop in storage.ts (the block using filter.labels, validateLabels, and the query.where(sql`json_extract(durably_runs.labels, ${`$.${key}`})`, ...)) to build a quoted JSON path segment instead of $.key — e.g. produce $."KEY" for the key and escape any internal double-quotes in KEY (replace " with \") so keys like app.kubernetes.io/name match as a single key; keep using the same query.where(sql`json_extract(durably_runs.labels, ${...})`, '=','...') call but supply the correctly quoted/escaped path string.
🧹 Nitpick comments (1)
packages/durably/tests/shared/storage.shared.ts (1)
243-268: Add a regression test for filtering Kubernetes-style label keys.Line 305 validates storage/round-trip, but there’s no assertion that
getRuns({ labels })works with keys likeapp.kubernetes.io/name. Adding that test would catch query-path regressions early.🧪 Suggested test addition
+ it('filters runs by Kubernetes-style label key', async () => { + await durably.storage.createRun({ + jobName: 'test-job', + payload: {}, + labels: { 'app.kubernetes.io/name': 'img-proc', env: 'prod' }, + }) + await durably.storage.createRun({ + jobName: 'test-job', + payload: {}, + labels: { 'app.kubernetes.io/name': 'data-sync', env: 'prod' }, + }) + + const runs = await durably.storage.getRuns({ + labels: { 'app.kubernetes.io/name': 'img-proc' }, + }) + expect(runs).toHaveLength(1) + expect(runs[0].labels).toEqual({ + 'app.kubernetes.io/name': 'img-proc', + env: 'prod', + }) + })Also applies to: 305-316
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/durably/tests/shared/storage.shared.ts` around lines 243 - 268, Add a regression test that verifies getRuns correctly filters by Kubernetes-style label keys containing dots (e.g., "app.kubernetes.io/name"); in the existing test suite (packages/durably/tests/shared/storage.shared.ts) createRun three runs with labels including one using "app.kubernetes.io/name" and then call durably.storage.getRuns({ labels: { "app.kubernetes.io/name": "<value>", ... } }) and assert exactly the expected run is returned and its labels match; ensure you use the same functions referenced in the file (createRun and getRuns) and include assertions for length and equality of the returned run.labels to catch any query-path regression with dotted label keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@packages/durably/src/storage.ts`:
- Around line 380-385: The JSON path built for json_extract in the labels
filtering loop treats dots as path separators and breaks dotted keys; update the
loop in storage.ts (the block using filter.labels, validateLabels, and the
query.where(sql`json_extract(durably_runs.labels, ${`$.${key}`})`, ...)) to
build a quoted JSON path segment instead of $.key — e.g. produce $."KEY" for the
key and escape any internal double-quotes in KEY (replace " with \") so keys
like app.kubernetes.io/name match as a single key; keep using the same
query.where(sql`json_extract(durably_runs.labels, ${...})`, '=','...') call but
supply the correctly quoted/escaped path string.
---
Nitpick comments:
In `@packages/durably/tests/shared/storage.shared.ts`:
- Around line 243-268: Add a regression test that verifies getRuns correctly
filters by Kubernetes-style label keys containing dots (e.g.,
"app.kubernetes.io/name"); in the existing test suite
(packages/durably/tests/shared/storage.shared.ts) createRun three runs with
labels including one using "app.kubernetes.io/name" and then call
durably.storage.getRuns({ labels: { "app.kubernetes.io/name": "<value>", ... }
}) and assert exactly the expected run is returned and its labels match; ensure
you use the same functions referenced in the file (createRun and getRuns) and
include assertions for length and equality of the returned run.labels to catch
any query-path regression with dotted label keys.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1274ea12-7e29-44bb-9e83-3de2111dfca4
📒 Files selected for processing (2)
packages/durably/src/storage.tspackages/durably/tests/shared/storage.shared.ts
- Fix json_extract bracket notation for dotted label keys ($."key") - Extract LabelChips component to deduplicate label rendering in dashboards - Add language identifiers to fenced code blocks in docs - Assert labels field in events test - Export LATEST_SCHEMA_VERSION from migrations - Document multi-label AND semantics in http-handler - Add doc-check skill to CLAUDE.md - Add regression test for filtering by dotted label keys Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
labels: Record<string, string>to runs for metadata tagging and filteringgetRuns(),useRuns(), and SSE subscriptionslabelsfield toClientRunandRunRecordtypes-,_,.,/only)Closes #27
Test plan
pnpm --filter example-browser-vite-react typecheckpnpm --filter example-browser-react-router-spa typecheckpnpm --filter example-fullstack-react-router typecheckpnpm exec tsc -p examples/server-node/tsconfig.json --noEmitpnpm test(core tests pass; browser tests require Playwright install)pnpm formatpasses🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes