Skip to content

feat: add Kubernetes-style labels for run filtering#31

Merged
coji merged 6 commits into
mainfrom
feat/labels-filtering
Mar 4, 2026
Merged

feat: add Kubernetes-style labels for run filtering#31
coji merged 6 commits into
mainfrom
feat/labels-filtering

Conversation

@coji
Copy link
Copy Markdown
Owner

@coji coji commented Mar 4, 2026

Summary

  • Add labels: Record<string, string> to runs for metadata tagging and filtering
  • Support label-based filtering in getRuns(), useRuns(), and SSE subscriptions
  • Add labels to browser example trigger calls (Image Processing with labels, Data Sync without for contrast)
  • Display labels in all three dashboard components (table column + modal)
  • Add labels field to ClientRun and RunRecord types
  • Validate label keys (alphanumeric, -, _, ., / only)
  • Update all documentation with simple examples first, then multi-tenancy pattern
  • Remove unused openspec directory; add doc-check skill

Closes #27

Test plan

  • pnpm --filter example-browser-vite-react typecheck
  • pnpm --filter example-browser-react-router-spa typecheck
  • pnpm --filter example-fullstack-react-router typecheck
  • pnpm exec tsc -p examples/server-node/tsconfig.json --noEmit
  • pnpm test (core tests pass; browser tests require Playwright install)
  • pnpm format passes
  • Label key validation tests (invalid keys, special chars, Kubernetes-style keys)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added labels support: attach metadata key-value pairs when triggering jobs for better organization and tracking.
    • Added label-based filtering: filter runs by labels in dashboards, API queries, and subscriptions.
    • Updated run dashboards to display labels as badges for improved visibility.

coji and others added 4 commits March 4, 2026 21:33
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>
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 4, 2026

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

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 4, 2026

Warning

Rate limit exceeded

@coji has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 39 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb229fbb-1f24-4071-863f-1a3ace1f0934

📥 Commits

Reviewing files that changed from the base of the PR and between 08bf967 and 147cc1a.

📒 Files selected for processing (12)
  • CLAUDE.md
  • examples/browser-react-router-spa/app/routes/_index/dashboard.tsx
  • examples/browser-vite-react/src/components/dashboard.tsx
  • examples/fullstack-react-router/app/routes/_index/dashboard.tsx
  • packages/durably/docs/llms.md
  • packages/durably/src/migrations.ts
  • packages/durably/src/storage.ts
  • packages/durably/tests/shared/events.shared.ts
  • packages/durably/tests/shared/storage.shared.ts
  • website/api/http-handler.md
  • website/guide/concepts.md
  • website/public/llms.txt
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Core Schema & Storage
packages/durably/src/schema.ts, packages/durably/src/migrations.ts, packages/durably/src/storage.ts, packages/durably/src/job.ts
Introduce labels column (JSON) and migration; add labels to CreateRunInput/Run/RunFilter; validate label keys; serialize/deserialize labels; apply per-key json_extract filters in getRuns; propagate labels on create/batch create.
Event Types & Emissions
packages/durably/src/events.ts, packages/durably/src/context.ts, packages/durably/src/durably.ts, packages/durably/src/worker.ts
Add labels: Record<string,string> to run/step event interfaces and include labels in emitted events (trigger, start, complete, fail, cancel, retry, progress, step events).
Server / HTTP / SSE
packages/durably/src/server.ts, website/api/http-handler.md
Expose labels on TriggerRequest and RunsRequest; parse label.* query params; implement label filtering for getRuns and SSE event streams; ensure consistent event filtering and SSE payloads.
React Client & Hooks
packages/durably-react/src/client/use-runs.ts, packages/durably-react/src/hooks/use-runs.ts, packages/durably-react/src/client/use-run-actions.ts, packages/durably-react/src/types.ts
Add labels on run types and options; add labels filtering to client hooks (UseRunsClientOptions/UseRunsOptions); memoize labels via useMemo and append label params to fetch/SSE URLs.
API Docs & Website
packages/durably/docs/llms.md, website/api/*, website/guide/concepts.md, website/public/llms.txt
Document label fields on TriggerRequest, TriggerOptions, Run, RunFilter, and events; add examples for triggering with labels and filtering runs by labels; update related docs/tables and examples.
Examples & UI
examples/.../_index.tsx, examples/.../dashboard.tsx, examples/browser-vite-react/src/App.tsx, examples/browser-vite-react/src/components/dashboard.tsx
Trigger calls updated to pass labels (e.g., { source: 'browser' } / { source: 'server' }); UI adds Labels column and labels display in run details; minor classname/layout reorders.
Tests & Test Helpers
packages/durably/tests/shared/storage.shared.ts, packages/durably/tests/shared/events.shared.ts, packages/durably/tests/shared/migrate.shared.ts, packages/durably-react/tests/client/use-runs.test.tsx
Add tests for labels persistence, defaulting, single/multi-key filtering, validation of keys; update emitted-event expectations to include labels; update migration test expectations to include new migration.
OpenSpec Removal
openspec/... (many files), AGENTS.md, CLAUDE.md, openspec/project.md
Remove large set of OpenSpec/spec/proposal/task files (HITL, job-versioning, postgresql, streaming-v2, core/react specs and project docs); remove OpenSpec instruction blocks from CLAUDE.md/AGENTS.md.
Doc-check / Release-check SKILLs
.claude/skills/doc-check/SKILL.md, .claude/skills/release-check/SKILL.md
Add new documentation update checklist SKILL; adjust release-check SKILL formatting minorly.

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)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I stitched small tags onto every run,
Tiny keys and values — now tidy, now fun.
From trigger down to storage deep,
Events carry labels that hop and leap.
Filters hum softly, tenants cheer — hooray for metadata sun!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes label-related changes that are in-scope (#27), but also removes the entire openspec directory and multiple documentation files not directly related to labels filtering. Separate the removal of openspec files and OpenSpec-related documentation changes into a distinct PR. Keep this PR focused on label-based filtering implementation for multi-tenant support.
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add Kubernetes-style labels for run filtering' accurately describes the main change: introducing a label-based filtering system for runs.
Linked Issues check ✅ Passed The PR implements label-based filtering for #27, which requested payload-based filtering for multi-tenant applications. The implementation addresses all core requirements: adds labels to runs, enables filtering in getRuns/useRuns, and applies filters to SSE subscriptions.

✏️ 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/labels-filtering

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.

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>
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: 3

🧹 Nitpick comments (6)
examples/fullstack-react-router/app/routes/_index.tsx (1)

42-46: Consider adding labels to the dataSync trigger for consistency.

The processImage trigger includes labels with source: 'server', but dataSync does 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: Add doc-check to 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: Assert labels in 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

📥 Commits

Reviewing files that changed from the base of the PR and between bbfbccb and 1d6ec95.

📒 Files selected for processing (55)
  • .claude/skills/doc-check/SKILL.md
  • .claude/skills/release-check/SKILL.md
  • AGENTS.md
  • CLAUDE.md
  • examples/browser-react-router-spa/app/routes/_index.tsx
  • examples/browser-react-router-spa/app/routes/_index/dashboard.tsx
  • examples/browser-vite-react/src/App.tsx
  • examples/browser-vite-react/src/components/dashboard.tsx
  • examples/fullstack-react-router/app/routes/_index.tsx
  • examples/fullstack-react-router/app/routes/_index/dashboard.tsx
  • openspec/AGENTS.md
  • openspec/changes/add-human-in-the-loop/proposal.md
  • openspec/changes/add-human-in-the-loop/specs/core/spec.md
  • openspec/changes/add-human-in-the-loop/specs/react/spec.md
  • openspec/changes/add-human-in-the-loop/tasks.md
  • openspec/changes/add-job-versioning/proposal.md
  • openspec/changes/add-job-versioning/specs/core/spec.md
  • openspec/changes/add-job-versioning/tasks.md
  • openspec/changes/add-postgresql/proposal.md
  • openspec/changes/add-postgresql/specs/core/spec.md
  • openspec/changes/add-postgresql/tasks.md
  • openspec/changes/add-streaming-v2/proposal.md
  • openspec/changes/add-streaming-v2/specs/core/spec.md
  • openspec/changes/add-streaming-v2/tasks.md
  • openspec/project.md
  • openspec/specs/core/spec.md
  • openspec/specs/react/spec.md
  • packages/durably-react/docs/llms.md
  • packages/durably-react/src/client/use-run-actions.ts
  • packages/durably-react/src/client/use-runs.ts
  • packages/durably-react/src/hooks/use-runs.ts
  • packages/durably-react/src/types.ts
  • packages/durably-react/tests/client/use-runs.test.tsx
  • packages/durably/docs/llms.md
  • packages/durably/src/context.ts
  • packages/durably/src/durably.ts
  • packages/durably/src/events.ts
  • packages/durably/src/job.ts
  • packages/durably/src/migrations.ts
  • packages/durably/src/schema.ts
  • packages/durably/src/server.ts
  • packages/durably/src/storage.ts
  • packages/durably/src/worker.ts
  • packages/durably/tests/shared/events.shared.ts
  • packages/durably/tests/shared/migrate.shared.ts
  • packages/durably/tests/shared/storage.shared.ts
  • website/api/create-durably.md
  • website/api/durably-react/browser.md
  • website/api/durably-react/client.md
  • website/api/durably-react/types.md
  • website/api/events.md
  • website/api/http-handler.md
  • website/api/index.md
  • website/guide/concepts.md
  • website/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

Comment thread packages/durably/docs/llms.md Outdated
Comment thread packages/durably/src/storage.ts
Comment thread website/guide/concepts.md Outdated
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 (1)
packages/durably/src/storage.ts (1)

380-385: ⚠️ Potential issue | 🟠 Major

Label filtering path breaks for dotted keys (still unresolved).

At Line 384, $.${key} treats . as path traversal, so valid keys like app.kubernetes.io/name won’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 like app.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

📥 Commits

Reviewing files that changed from the base of the PR and between 1d6ec95 and 08bf967.

📒 Files selected for processing (2)
  • packages/durably/src/storage.ts
  • packages/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>
@coji coji merged commit aaa3306 into main Mar 4, 2026
4 checks passed
@coji coji deleted the feat/labels-filtering branch March 5, 2026 12:14
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.

Support payload-based filtering in getRuns / useRuns

1 participant