From b17ef5960456e4aa502e2cb4dab973dd2d2e19b7 Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Sat, 4 Jul 2026 16:39:28 +0100 Subject: [PATCH 1/3] fix(webapp): accept all valid run and batch IDs in dashboard filters The Run ID and Batch ID filter inputs on the runs list, batches list, and logs view hard-coded the exact friendly-id lengths. Run and batch IDs in the current format are two characters longer, so they were rejected with a "wrong length" error and the Apply button stayed disabled, making it impossible to filter by a recently created run or batch ID from the UI. These filters now validate IDs with the shared id-shape classifier instead of hard-coded lengths, so they accept every valid ID and will not drift if the format changes again. The waitpoint ID filter gets the same fix pre-emptively. --- .server-changes/fix-ksuid-id-filters.md | 6 ++++++ apps/webapp/app/components/logs/LogsRunIdFilter.tsx | 5 +++-- apps/webapp/app/components/runs/v3/BatchFilters.tsx | 3 ++- apps/webapp/app/components/runs/v3/RunFilters.tsx | 5 +++-- .../webapp/app/components/runs/v3/WaitpointTokenFilters.tsx | 3 ++- 5 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 .server-changes/fix-ksuid-id-filters.md diff --git a/.server-changes/fix-ksuid-id-filters.md b/.server-changes/fix-ksuid-id-filters.md new file mode 100644 index 00000000000..512dd22109f --- /dev/null +++ b/.server-changes/fix-ksuid-id-filters.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +The Run ID and Batch ID filters on the runs, batches, and logs pages now accept all valid run and batch IDs, fixing a case where recently created IDs were incorrectly rejected. diff --git a/apps/webapp/app/components/logs/LogsRunIdFilter.tsx b/apps/webapp/app/components/logs/LogsRunIdFilter.tsx index e23c39534a6..2bbaac25297 100644 --- a/apps/webapp/app/components/logs/LogsRunIdFilter.tsx +++ b/apps/webapp/app/components/logs/LogsRunIdFilter.tsx @@ -1,5 +1,6 @@ import * as Ariakit from "@ariakit/react"; import { FingerPrintIcon } from "@heroicons/react/20/solid"; +import { isClassifiable } from "@trigger.dev/core/v3/isomorphic"; import { useCallback, useState } from "react"; import { AppliedFilter } from "~/components/primitives/AppliedFilter"; import { Button } from "~/components/primitives/Buttons"; @@ -72,8 +73,8 @@ function RunIdDropdown({ if (runId) { if (!runId.startsWith("run_")) { error = "Run IDs start with 'run_'"; - } else if (runId.length !== 25 && runId.length !== 29) { - error = "Run IDs are 25 or 29 characters long"; + } else if (!isClassifiable(runId)) { + error = "That doesn't look like a valid run ID"; } } diff --git a/apps/webapp/app/components/runs/v3/BatchFilters.tsx b/apps/webapp/app/components/runs/v3/BatchFilters.tsx index cb7e7c13ad0..efde58d7a75 100644 --- a/apps/webapp/app/components/runs/v3/BatchFilters.tsx +++ b/apps/webapp/app/components/runs/v3/BatchFilters.tsx @@ -1,6 +1,7 @@ import * as Ariakit from "@ariakit/react"; import { Squares2X2Icon, XMarkIcon } from "@heroicons/react/20/solid"; import { Form } from "@remix-run/react"; +import { isClassifiable } from "@trigger.dev/core/v3/isomorphic"; import type { BatchTaskRunStatus } from "@trigger.dev/database"; import { type ReactNode, useRef } from "react"; import { z } from "zod"; @@ -227,7 +228,7 @@ function PermanentStatusFilter() { function validateBatchId(value: string): string | undefined { if (!value.startsWith("batch_")) return "Batch IDs start with 'batch_'"; - if (value.length !== 27 && value.length !== 31) return "Batch IDs are 27 or 31 characters long"; + if (!isClassifiable(value)) return "That doesn't look like a valid batch ID"; } function BatchIdDropdown( diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 6917f07b104..557a6b16bd1 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -13,6 +13,7 @@ import { import { Form, useFetcher } from "@remix-run/react"; import { IconRotateClockwise2, IconToggleLeft } from "@tabler/icons-react"; import { MachinePresetName } from "@trigger.dev/core/v3"; +import { isClassifiable } from "@trigger.dev/core/v3/isomorphic"; import type { BulkActionType, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database"; import { matchSorter } from "match-sorter"; import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; @@ -1715,7 +1716,7 @@ function RootOnlyToggle({ defaultValue }: { defaultValue: boolean }) { function validateRunId(value: string): string | undefined { if (!value.startsWith("run_")) return "Run IDs start with 'run_'"; - if (value.length !== 25 && value.length !== 29) return "Run IDs are 25 or 29 characters long"; + if (!isClassifiable(value)) return "That doesn't look like a valid run ID"; } function RunIdDropdown( @@ -1770,7 +1771,7 @@ function AppliedRunIdFilter() { function validateBatchId(value: string): string | undefined { if (!value.startsWith("batch_")) return "Batch IDs start with 'batch_'"; - if (value.length !== 27 && value.length !== 31) return "Batch IDs are 27 or 31 characters long"; + if (!isClassifiable(value)) return "That doesn't look like a valid batch ID"; } function BatchIdDropdown( diff --git a/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx b/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx index 5a8c7a82176..70473bc2e55 100644 --- a/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx +++ b/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx @@ -2,6 +2,7 @@ import * as Ariakit from "@ariakit/react"; import { FingerPrintIcon, TagIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { Form, useFetcher } from "@remix-run/react"; import { WaitpointTokenStatus, waitpointTokenStatuses } from "@trigger.dev/core/v3"; +import { isClassifiable } from "@trigger.dev/core/v3/isomorphic"; import { ListChecks } from "lucide-react"; import { matchSorter } from "match-sorter"; import { type ReactNode, useEffect, useMemo, useRef } from "react"; @@ -409,7 +410,7 @@ function WaitpointIdDropdown( paramKey="id" validate={(v) => { if (!v.startsWith("waitpoint_")) return "Waitpoint IDs start with 'waitpoint_'"; - if (v.length !== 35) return "Waitpoint IDs are 35 characters long"; + if (!isClassifiable(v)) return "That doesn't look like a valid waitpoint ID"; return undefined; }} /> From 9a00d4f27e102d0b04d6fbd6132b45e9519e01fb Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Sat, 4 Jul 2026 17:14:57 +0100 Subject: [PATCH 2/3] refactor(webapp): consolidate id filter validators into a tested helper Replace the per-filter isClassifiable checks with a single makeFriendlyIdValidator helper covered by unit tests. isClassifiable only recognizes cuid and ksuid bodies, so it rejected legacy nanoid ids (run_ / batch_ + 21-char body) that the filters previously accepted; the helper accepts all three body lengths (21 nanoid, 25 cuid, 27 ksuid) and is now shared by the run, batch, waitpoint, and schedule filters. --- .server-changes/fix-ksuid-id-filters.md | 2 +- .../app/components/logs/LogsRunIdFilter.tsx | 12 +-- .../app/components/runs/v3/BatchFilters.tsx | 7 +- .../app/components/runs/v3/RunFilters.tsx | 17 +--- .../runs/v3/WaitpointTokenFilters.tsx | 10 +-- apps/webapp/app/utils/friendlyId.test.ts | 85 +++++++++++++++++++ apps/webapp/app/utils/friendlyId.ts | 31 +++++++ apps/webapp/vitest.config.ts | 1 + 8 files changed, 131 insertions(+), 34 deletions(-) create mode 100644 apps/webapp/app/utils/friendlyId.test.ts create mode 100644 apps/webapp/app/utils/friendlyId.ts diff --git a/.server-changes/fix-ksuid-id-filters.md b/.server-changes/fix-ksuid-id-filters.md index 512dd22109f..815abebbd0c 100644 --- a/.server-changes/fix-ksuid-id-filters.md +++ b/.server-changes/fix-ksuid-id-filters.md @@ -3,4 +3,4 @@ area: webapp type: fix --- -The Run ID and Batch ID filters on the runs, batches, and logs pages now accept all valid run and batch IDs, fixing a case where recently created IDs were incorrectly rejected. +The Run ID and Batch ID filters on the runs, batches, and logs pages now accept every valid ID format, fixing a case where valid IDs were rejected and the Apply button stayed disabled. diff --git a/apps/webapp/app/components/logs/LogsRunIdFilter.tsx b/apps/webapp/app/components/logs/LogsRunIdFilter.tsx index 2bbaac25297..a2e52a1a0f7 100644 --- a/apps/webapp/app/components/logs/LogsRunIdFilter.tsx +++ b/apps/webapp/app/components/logs/LogsRunIdFilter.tsx @@ -1,6 +1,5 @@ import * as Ariakit from "@ariakit/react"; import { FingerPrintIcon } from "@heroicons/react/20/solid"; -import { isClassifiable } from "@trigger.dev/core/v3/isomorphic"; import { useCallback, useState } from "react"; import { AppliedFilter } from "~/components/primitives/AppliedFilter"; import { Button } from "~/components/primitives/Buttons"; @@ -10,8 +9,10 @@ import { Label } from "~/components/primitives/Label"; import { SelectPopover, SelectProvider, SelectTrigger } from "~/components/primitives/Select"; import { useSearchParams } from "~/hooks/useSearchParam"; import { FilterMenuProvider } from "~/components/runs/v3/SharedFilters"; +import { makeFriendlyIdValidator } from "~/utils/friendlyId"; const shortcut = { key: "i" }; +const validateRunId = makeFriendlyIdValidator("run", "Run"); export function LogsRunIdFilter() { const { value } = useSearchParams(); @@ -69,14 +70,7 @@ function RunIdDropdown({ setOpen(false); }, [runId, replace, clearSearchValue]); - let error: string | undefined = undefined; - if (runId) { - if (!runId.startsWith("run_")) { - error = "Run IDs start with 'run_'"; - } else if (!isClassifiable(runId)) { - error = "That doesn't look like a valid run ID"; - } - } + const error = runId ? validateRunId(runId) : undefined; return ( diff --git a/apps/webapp/app/components/runs/v3/BatchFilters.tsx b/apps/webapp/app/components/runs/v3/BatchFilters.tsx index efde58d7a75..83766ce2d6c 100644 --- a/apps/webapp/app/components/runs/v3/BatchFilters.tsx +++ b/apps/webapp/app/components/runs/v3/BatchFilters.tsx @@ -1,7 +1,6 @@ import * as Ariakit from "@ariakit/react"; import { Squares2X2Icon, XMarkIcon } from "@heroicons/react/20/solid"; import { Form } from "@remix-run/react"; -import { isClassifiable } from "@trigger.dev/core/v3/isomorphic"; import type { BatchTaskRunStatus } from "@trigger.dev/database"; import { type ReactNode, useRef } from "react"; import { z } from "zod"; @@ -25,6 +24,7 @@ import { import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; import { useSearchParams } from "~/hooks/useSearchParam"; import { useShortcutKeys } from "~/hooks/useShortcutKeys"; +import { makeFriendlyIdValidator } from "~/utils/friendlyId"; import { Button } from "../../primitives/Buttons"; import { allBatchStatuses, @@ -226,10 +226,7 @@ function PermanentStatusFilter() { ); } -function validateBatchId(value: string): string | undefined { - if (!value.startsWith("batch_")) return "Batch IDs start with 'batch_'"; - if (!isClassifiable(value)) return "That doesn't look like a valid batch ID"; -} +const validateBatchId = makeFriendlyIdValidator("batch", "Batch"); function BatchIdDropdown( props: Omit diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 557a6b16bd1..f92e92c2bb8 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -13,7 +13,6 @@ import { import { Form, useFetcher } from "@remix-run/react"; import { IconRotateClockwise2, IconToggleLeft } from "@tabler/icons-react"; import { MachinePresetName } from "@trigger.dev/core/v3"; -import { isClassifiable } from "@trigger.dev/core/v3/isomorphic"; import type { BulkActionType, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database"; import { matchSorter } from "match-sorter"; import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; @@ -66,6 +65,7 @@ import { useShortcutKeys } from "~/hooks/useShortcutKeys"; import { type loader as tagsLoader } from "~/routes/resources.environments.$envId.runs.tags"; import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues"; import { type loader as versionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions"; +import { makeFriendlyIdValidator } from "~/utils/friendlyId"; import { Button } from "../../primitives/Buttons"; import { AIFilterInput } from "./AIFilterInput"; import { BulkActionTypeCombo } from "./BulkAction"; @@ -1714,10 +1714,7 @@ function RootOnlyToggle({ defaultValue }: { defaultValue: boolean }) { ); } -function validateRunId(value: string): string | undefined { - if (!value.startsWith("run_")) return "Run IDs start with 'run_'"; - if (!isClassifiable(value)) return "That doesn't look like a valid run ID"; -} +const validateRunId = makeFriendlyIdValidator("run", "Run"); function RunIdDropdown( props: Omit< @@ -1769,10 +1766,7 @@ function AppliedRunIdFilter() { ); } -function validateBatchId(value: string): string | undefined { - if (!value.startsWith("batch_")) return "Batch IDs start with 'batch_'"; - if (!isClassifiable(value)) return "That doesn't look like a valid batch ID"; -} +const validateBatchId = makeFriendlyIdValidator("batch", "Batch"); function BatchIdDropdown( props: Omit @@ -1820,10 +1814,7 @@ function AppliedBatchIdFilter() { ); } -function validateScheduleId(value: string): string | undefined { - if (!value.startsWith("sched_")) return "Schedule IDs start with 'sched_'"; - if (value.length !== 27) return "Schedule IDs are 27 characters long"; -} +const validateScheduleId = makeFriendlyIdValidator("sched", "Schedule"); function ScheduleIdDropdown( props: Omit diff --git a/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx b/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx index 70473bc2e55..a0f6e1a59f4 100644 --- a/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx +++ b/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx @@ -2,7 +2,6 @@ import * as Ariakit from "@ariakit/react"; import { FingerPrintIcon, TagIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { Form, useFetcher } from "@remix-run/react"; import { WaitpointTokenStatus, waitpointTokenStatuses } from "@trigger.dev/core/v3"; -import { isClassifiable } from "@trigger.dev/core/v3/isomorphic"; import { ListChecks } from "lucide-react"; import { matchSorter } from "match-sorter"; import { type ReactNode, useEffect, useMemo, useRef } from "react"; @@ -34,6 +33,7 @@ import { useProject } from "~/hooks/useProject"; import { useSearchParams } from "~/hooks/useSearchParam"; import { useShortcutKeys } from "~/hooks/useShortcutKeys"; import { type loader as tagsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tags"; +import { makeFriendlyIdValidator } from "~/utils/friendlyId"; import { appliedSummary, FilterMenuProvider, @@ -399,6 +399,8 @@ function PermanentTagsFilter() { ); } +const validateWaitpointId = makeFriendlyIdValidator("waitpoint", "Waitpoint"); + function WaitpointIdDropdown( props: Omit ) { @@ -408,11 +410,7 @@ function WaitpointIdDropdown( label="Waitpoint ID" placeholder="waitpoint_" paramKey="id" - validate={(v) => { - if (!v.startsWith("waitpoint_")) return "Waitpoint IDs start with 'waitpoint_'"; - if (!isClassifiable(v)) return "That doesn't look like a valid waitpoint ID"; - return undefined; - }} + validate={validateWaitpointId} /> ); } diff --git a/apps/webapp/app/utils/friendlyId.test.ts b/apps/webapp/app/utils/friendlyId.test.ts new file mode 100644 index 00000000000..4b2854f51c2 --- /dev/null +++ b/apps/webapp/app/utils/friendlyId.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from "vitest"; +import { + BatchId, + generateFriendlyId, + generateKsuidId, + RunId, +} from "@trigger.dev/core/v3/isomorphic"; +import { isValidFriendlyId, makeFriendlyIdValidator } from "./friendlyId"; + +describe("isValidFriendlyId", () => { + it("accepts every id generation the real generators produce", () => { + // nanoid (legacy V1), cuid (run-engine), ksuid (run-ops split) + expect(isValidFriendlyId(generateFriendlyId("run"), "run")).toBe(true); + expect(isValidFriendlyId(RunId.generate().friendlyId, "run")).toBe(true); + expect(isValidFriendlyId(RunId.toFriendlyId(generateKsuidId()), "run")).toBe(true); + + expect(isValidFriendlyId(generateFriendlyId("batch"), "batch")).toBe(true); + expect(isValidFriendlyId(BatchId.generate().friendlyId, "batch")).toBe(true); + expect(isValidFriendlyId(BatchId.toFriendlyId(generateKsuidId()), "batch")).toBe(true); + }); + + it("accepts each valid body length (21 nanoid, 25 cuid, 27 ksuid)", () => { + expect(isValidFriendlyId("run_" + "a".repeat(21), "run")).toBe(true); + expect(isValidFriendlyId("run_" + "a".repeat(25), "run")).toBe(true); + expect(isValidFriendlyId("run_" + "a".repeat(27), "run")).toBe(true); + }); + + it("accepts mixed-case (uppercase) ksuid bodies", () => { + expect(isValidFriendlyId("run_2ABCdefGHI0123456789jklMN", "run")).toBe(true); + }); + + it("rejects the wrong prefix", () => { + expect(isValidFriendlyId(RunId.generate().friendlyId, "batch")).toBe(false); + expect(isValidFriendlyId("batch_" + "a".repeat(25), "run")).toBe(false); + }); + + it("rejects a bare (unprefixed) id", () => { + expect(isValidFriendlyId("a".repeat(25), "run")).toBe(false); + }); + + it("rejects body lengths that match no generator", () => { + for (const len of [0, 20, 22, 24, 26, 28]) { + expect(isValidFriendlyId("run_" + "a".repeat(len), "run")).toBe(false); + } + }); + + it("rejects non-base62 characters in the body", () => { + expect(isValidFriendlyId("run_" + "-".repeat(25), "run")).toBe(false); + expect(isValidFriendlyId("run_" + "!".repeat(25), "run")).toBe(false); + // an underscore in the body is not base62 + expect(isValidFriendlyId("run_" + "a".repeat(24) + "_", "run")).toBe(false); + }); + + it("does not treat the prefix separator as optional", () => { + // "runX..." shares the "run" prefix but not the "run_" marker + expect(isValidFriendlyId("run" + "a".repeat(25), "run")).toBe(false); + }); +}); + +describe("makeFriendlyIdValidator", () => { + const validateRunId = makeFriendlyIdValidator("run", "Run"); + const validateBatchId = makeFriendlyIdValidator("batch", "Batch"); + + it("returns undefined for a valid id of any generation", () => { + expect(validateRunId(generateFriendlyId("run"))).toBeUndefined(); + expect(validateRunId(RunId.generate().friendlyId)).toBeUndefined(); + expect(validateRunId(RunId.toFriendlyId(generateKsuidId()))).toBeUndefined(); + expect(validateBatchId(BatchId.toFriendlyId(generateKsuidId()))).toBeUndefined(); + }); + + it("reports a wrong prefix distinctly from a wrong shape", () => { + expect(validateRunId("batch_" + "a".repeat(25))).toBe("Run IDs start with 'run_'"); + expect(validateRunId("run_" + "a".repeat(20))).toBe("That doesn't look like a valid run ID"); + }); + + it("derives the marker and label per entity", () => { + const validateWaitpointId = makeFriendlyIdValidator("waitpoint", "Waitpoint"); + expect(validateWaitpointId("run_" + "a".repeat(25))).toBe( + "Waitpoint IDs start with 'waitpoint_'" + ); + expect(validateWaitpointId("waitpoint_" + "a".repeat(20))).toBe( + "That doesn't look like a valid waitpoint ID" + ); + }); +}); diff --git a/apps/webapp/app/utils/friendlyId.ts b/apps/webapp/app/utils/friendlyId.ts new file mode 100644 index 00000000000..bc26ef19b4b --- /dev/null +++ b/apps/webapp/app/utils/friendlyId.ts @@ -0,0 +1,31 @@ +import { CUID_LENGTH, KSUID_LENGTH } from "@trigger.dev/core/v3/isomorphic"; + +// The body after `_` is a base62 id; three generator lengths remain +// valid in existing data and must all be accepted: 21 (nanoid), 25 (cuid), +// 27 (ksuid). cuid/ksuid come from core so this tracks any future change. +const NANOID_BODY_LENGTH = 21; +const VALID_BODY_LENGTHS: ReadonlySet = new Set([ + NANOID_BODY_LENGTH, + CUID_LENGTH, + KSUID_LENGTH, +]); + +const BASE62 = /^[0-9A-Za-z]+$/; + +export function isValidFriendlyId(value: string, prefix: string): boolean { + const marker = `${prefix}_`; + if (!value.startsWith(marker)) return false; + const body = value.slice(marker.length); + return VALID_BODY_LENGTHS.has(body.length) && BASE62.test(body); +} + +export function makeFriendlyIdValidator(prefix: string, label: string) { + const marker = `${prefix}_`; + return (value: string): string | undefined => { + if (!value.startsWith(marker)) return `${label} IDs start with '${marker}'`; + if (!isValidFriendlyId(value, prefix)) { + return `That doesn't look like a valid ${label.toLowerCase()} ID`; + } + return undefined; + }; +} diff --git a/apps/webapp/vitest.config.ts b/apps/webapp/vitest.config.ts index 8e05aec1ebc..f9ec17fec0b 100644 --- a/apps/webapp/vitest.config.ts +++ b/apps/webapp/vitest.config.ts @@ -16,6 +16,7 @@ export default defineConfig({ "app/v3/services/bulk/**/*.test.ts", "app/runEngine/concerns/**/*.test.ts", "app/runEngine/services/**/*.test.ts", + "app/utils/**/*.test.ts", ], // *.e2e.test.ts: smoke matrix, run via vitest.e2e.config.ts. // *.e2e.full.test.ts: full auth suite, runs via vitest.e2e.full.config.ts From 3bfc9dd79e204411b6c35ce3005ad369d44cd15a Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Sat, 4 Jul 2026 17:35:59 +0100 Subject: [PATCH 3/3] docs(webapp): note why error id filter skips the shared validator --- apps/webapp/app/components/runs/v3/RunFilters.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index f92e92c2bb8..79ff2a73c71 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -1862,6 +1862,8 @@ function AppliedScheduleIdFilter() { ); } +// Error ids are `error_<16-char sha256 fingerprint>`, not a fixed-length generated +// id, so they intentionally skip makeFriendlyIdValidator (its length check would reject them). function validateErrorId(value: string): string | undefined { if (!value.startsWith("error_")) return "Error IDs start with 'error_'"; }