Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f956b28
chore: upgrade to zod v4
carderne Jun 30, 2026
5e38b25
format, typecheck, fix build
carderne Jun 30, 2026
178309a
fix failing test
carderne Jun 30, 2026
f78a354
format, lint
carderne Jul 3, 2026
753b413
cli dep zod4, update changelog
carderne Jul 3, 2026
53aa3a9
bump to platform@1.2.0
carderne Jul 3, 2026
1503317
updates
carderne Aug 3, 2026
1a33ab8
fixes
carderne Aug 3, 2026
a4361ce
fix test
carderne Aug 3, 2026
6de9f35
bump platform dep
carderne Aug 3, 2026
bab41c8
fix schema
carderne Aug 3, 2026
c2ad908
lint
carderne Aug 3, 2026
ceaba99
fix: preserve zod v4 configuration behavior
carderne Aug 3, 2026
019a833
fix: declare zod peer dependencies
carderne Aug 4, 2026
6764a08
fix: use zod-error v2 with zod 4
carderne Aug 4, 2026
7fbe648
fix: preserve empty task payloads and validation messages
carderne Aug 4, 2026
5e6e5df
improve cross-version zod support
carderne Aug 4, 2026
7ae57c9
fix ci
carderne Aug 4, 2026
25e91ef
verify omitted payload
carderne Aug 4, 2026
2c31e67
fix(core): preserve message payloads during validation
carderne Aug 4, 2026
15f47d7
fix: reconcile Zod 4 migration with current schemas
carderne Sep 3, 2026
5de7ab6
fix: stabilize alert form reset
carderne Sep 3, 2026
c885dc8
chore: bump runtime Zod to 4.5.4
carderne Sep 3, 2026
979bb11
fix(core): keep composed schemas on Zod 4
carderne Sep 3, 2026
dcc0fc0
test(core): assert compatibility bundle output exists
carderne Sep 3, 2026
8cc03df
improve zod v3 compat
carderne Sep 7, 2026
776196a
small fix
carderne Sep 7, 2026
6871a57
fix(cli): explain unsupported Zod versions
carderne Sep 7, 2026
5011bed
fix(core): infer Zod tool payloads without the optional ai package
carderne Sep 7, 2026
fa2801f
fix: support Zod 3.25.56 alongside Zod 4
carderne Sep 8, 2026
75eb2c3
test(core): use explicit Zod compatibility module paths
carderne Sep 8, 2026
e4bf3f1
test: refresh prompt prefix snapshot for Zod 4
carderne Sep 8, 2026
a00bfcb
fix agents.md
carderne Sep 8, 2026
502ccd8
fix(cli): link unsupported Zod errors to upgrade guidance
carderne Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/zod-4-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@trigger.dev/build": minor
"@trigger.dev/core": minor
"@trigger.dev/react-hooks": minor
"@trigger.dev/rsc": minor
"@trigger.dev/sdk": minor
"trigger.dev": minor
"@trigger.dev/redis-worker": minor
"@trigger.dev/schema-to-json": minor
---

Trigger.dev now uses Zod 4 by default. Projects using Zod 3.25.56 or later 3.x releases remain supported.

Zod remains a runtime dependency of packages that execute schemas, so existing and new installations continue to receive it automatically. The matching peer dependency range allows package managers to reuse either a compatible Zod 3 or Zod 4 installation from your project.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ When a **server-only** change (`apps/webapp/`, `apps/supervisor/`, etc., with no

## Dependency Pinning

Zod is pinned to a single version across the entire monorepo (currently `3.25.76`). When adding zod to a new or existing package, use the **exact same version** as the rest of the repo - never a different version or a range. Mismatched zod versions cause runtime type incompatibilities (e.g., schemas from one package can't be used as body validators in another).
Zod is pinned to a single version across the entire monorepo. When adding zod to a new or existing package, use the **exact same version** as the rest of the repo - never a different version or a range. Mismatched zod versions cause runtime type incompatibilities (e.g., schemas from one package can't be used as body validators in another).

## Architecture Overview

Expand Down
2 changes: 1 addition & 1 deletion apps/supervisor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"prom-client": "^15.1.0",
"socket.io": "4.8.3",
"std-env": "^3.8.0",
"zod": "3.25.76"
"zod": "4.5.4"
},
"devDependencies": {
"@internal/testcontainers": "workspace:*",
Expand Down
4 changes: 2 additions & 2 deletions apps/webapp/app/components/Feedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
getTextareaProps,
useForm,
} from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import { InformationCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid";
import { EnvelopeIcon, ShieldCheckIcon } from "@heroicons/react/24/solid";
import { Form, useActionData, useLocation, useNavigation, useSearchParams } from "@remix-run/react";
Expand Down Expand Up @@ -130,7 +130,7 @@ export function Feedback({
<Fieldset className="max-w-full gap-y-3">
<input
value={location.pathname}
{...getInputProps(fields.path, { type: "hidden" })}
{...getInputProps(fields.path, { type: "hidden", value: false })}
/>
<InputGroup className="max-w-full">
Comment thread
carderne marked this conversation as resolved.
{type === "feature" && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFormProps, getInputProps, useForm, type SubmissionResult } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import { PlusIcon, TrashIcon } from "@heroicons/react/20/solid";
import { Form, useActionData, useSearchParams } from "@remix-run/react";
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getFormProps, useForm, type SubmissionResult } from "@conform-to/react";

import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import { Form, useActionData } from "@remix-run/react";
import { useEffect, useMemo, useRef, useState } from "react";
import { z } from "zod";
Expand Down Expand Up @@ -38,7 +38,7 @@ export const billingLimitFormSchema = z.discriminatedUnion("mode", [
z.object({
mode: z.literal("custom"),
amount: z.coerce
.number({ invalid_type_error: "Not a valid amount" })
.number({ error: "Not a valid amount" })
.positive("Amount must be greater than 0"),
cancelInProgressRuns: z
.preprocess((v) => v === "on" || v === true || v === "true", z.boolean())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFormProps, useForm, type SubmissionResult } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { useEffect, useMemo, useRef, useState } from "react";
import { z } from "zod";
Expand All @@ -21,7 +21,7 @@ export const billingLimitRecoveryFormSchema = z
.object({
action: z.enum(["increase", "remove"]),
newAmount: z.coerce
.number({ invalid_type_error: "Not a valid amount" })
.number({ error: "Not a valid amount" })
.positive("Amount must be greater than 0")
.optional(),
resumeMode: z.enum(["queue", "new_only"]),
Expand Down Expand Up @@ -88,7 +88,7 @@ export function BillingLimitRecoveryPanel({
},
defaultValue: {
action: "increase",
newAmount: suggestedNewLimitDollars,
newAmount: String(suggestedNewLimitDollars),
resumeMode: "queue",
},
});
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { GlobeLinesIcon } from "~/assets/icons/GlobeLinesIcon";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import { EnvelopeIcon, HashtagIcon, LockClosedIcon, XMarkIcon } from "@heroicons/react/20/solid";
import { BellAlertIcon } from "@heroicons/react/24/solid";
import { useFetcher, useNavigate } from "@remix-run/react";
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/components/metrics/QueryWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const chartConfigOptions = {
sortByColumn: z.string().nullable(),
sortDirection: SortDirection,
aggregation: AggregationType,
seriesColors: z.record(z.string()).optional(),
seriesColors: z.record(z.string(), z.string()).optional(),
};

const ChartConfiguration = z.object({ ...chartConfigOptions });
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import { RectangleStackIcon } from "@heroicons/react/20/solid";
import { DialogClose } from "@radix-ui/react-dialog";
import { Form, useActionData, useNavigation, useParams, useSubmit } from "@remix-run/react";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFormProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import { EnvelopeIcon } from "@heroicons/react/20/solid";
import { DialogClose } from "@radix-ui/react-dialog";
import { useFetcher } from "@remix-run/react";
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/models/orgIntegration.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const SlackSecretSchema = z.object({
refreshToken: z.string().optional(),
botScopes: z.array(z.string()).optional(),
userScopes: z.array(z.string()).optional(),
raw: z.record(z.any()).optional(),
raw: z.record(z.string(), z.any()).optional(),
});

type SlackSecret = z.infer<typeof SlackSecretSchema>;
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/models/vercelIntegration.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ const VercelSecretSchema = z.object({
teamId: z.string().nullable().optional(),
userId: z.string().optional(),
installationId: z.string().optional(),
raw: z.record(z.any()).optional(),
raw: z.record(z.string(), z.any()).optional(),
});

type VercelSecret = z.infer<typeof VercelSecretSchema>;
Expand Down
4 changes: 2 additions & 2 deletions apps/webapp/app/models/vercelSdkRecovery.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,12 @@ export const VercelSchemas = {
.union([
z
.object({
envs: z.array(z.record(z.unknown())),
envs: z.array(z.record(z.string(), z.unknown())),
pagination: z.unknown().optional(),
hiddenProductionEnvCount: z.number().optional(),
})
.passthrough(),
z.array(z.record(z.unknown())),
z.array(z.record(z.string(), z.unknown())),
])
.transform((val) => (Array.isArray(val) ? { envs: val } : val)),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const DashboardLayout = z.discriminatedUnion("version", [
z.object({
version: z.literal("1"),
layout: z.array(LayoutItem),
widgets: z.record(Widget),
widgets: z.record(z.string(), Widget),
}),
]);

Expand Down
3 changes: 3 additions & 0 deletions apps/webapp/app/presenters/v3/SpanPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,9 @@ export class SpanPresenter extends BasePresenter {
filePath: lockedWorker?.lockedBy?.filePath ?? "",
},
run: {
// zod v4 types the run-context `context` (z.any) field as required; it was
// never populated here (optional under v3), so undefined preserves behavior.
context: undefined,
id: run.friendlyId,
createdAt: run.createdAt,
tags: run.runTags,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import {
ArrowUpCircleIcon,
EnvelopeIcon,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import { HashtagIcon, LockClosedIcon } from "@heroicons/react/20/solid";
import { Form, useActionData, useNavigate, useNavigation } from "@remix-run/react";
import { type LoaderFunctionArgs } from "@remix-run/router";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getFormProps, useForm } from "@conform-to/react";
import { GlobeLinesIcon } from "~/assets/icons/GlobeLinesIcon";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import {
BellAlertIcon,
BellSlashIcon,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import { ArrowUpCircleIcon, CheckIcon, EnvelopeIcon, PlusIcon } from "@heroicons/react/20/solid";
import { BookOpenIcon } from "@heroicons/react/24/solid";
import { DialogClose } from "@radix-ui/react-dialog";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import {
ArrowDownIcon,
EnvelopeIcon,
Expand Down Expand Up @@ -475,7 +475,7 @@ function Upgradable({
</TableRow>
</TableBody>
</Table>
<FormError id={formEnvironments.id}>{formEnvironments.errors}</FormError>
<FormError id={formEnvironments.errorId}>{formEnvironments.errors}</FormError>
</div>
<Form className="flex flex-col gap-2" method="post" {...getFormProps(form)} id="allocate">
<input type="hidden" name="action" value="allocate" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFormProps, useForm, type FieldMetadata, type FormMetadata } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import {
LockClosedIcon,
LockOpenIcon,
Expand Down Expand Up @@ -91,7 +91,7 @@ const schema = z.object({

return;
},
z.array(z.string(), { required_error: "At least one environment is required" })
z.array(z.string(), { error: "At least one environment is required" })
),
variables: z.preprocess((i) => {
if (!Array.isArray(i)) {
Expand Down Expand Up @@ -250,9 +250,9 @@ export default function Page() {
const [selectedEnvironmentIds, setSelectedEnvironmentIds] = useState<Set<string>>(new Set());
const [selectedBranchId, setSelectedBranchId] = useState<string | undefined>(undefined);

// TODO for no we only support branch-specific env vars for Preview environments
// Mostly to keep the UX for setting consistent env-vars across Dev/Staging/Prod easier
const previewBranches = environments.filter(
// TODO: we only support branch-specific env vars for Preview environments.
// This keeps setting consistent env vars across Dev/Staging/Prod easier.
const branchEnvironments = environments.filter(
(env) => env.type === "PREVIEW" && env.parentEnvironmentId !== null
);
const nonBranchEnvironments = environments.filter((env) => env.parentEnvironmentId === null);
Expand Down Expand Up @@ -439,15 +439,15 @@ export default function Page() {
value={selectedBranchId ?? "all"}
setValue={handleBranchChange}
placeholder="All branches"
items={[{ id: "all", branchName: "All branches" }, ...previewBranches]}
items={[{ id: "all", branchName: "All branches" }, ...branchEnvironments]}
className="w-fit min-w-52"
filter={{
keys: [
(item) => item.branchName?.replace(/\//g, " ").replace(/_/g, " ") ?? "",
],
}}
text={(val) =>
val ? previewBranches.find((b) => b.id === val)?.branchName : null
val ? branchEnvironments.find((b) => b.id === val)?.branchName : null
}
dropdownIcon
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import {
BookOpenIcon,
InformationCircleIcon,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import { BellAlertIcon } from "@heroicons/react/20/solid";
import { useFetcher, useRevalidator } from "@remix-run/react";
import { type ActionFunctionArgs, json, type LoaderFunctionArgs } from "@remix-run/server-runtime";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import { Outlet, useNavigate } from "@remix-run/react";
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { useCallback } from "react";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
if (!parsed.success) {
return typedjson(
{
error: parsed.error.errors.map((e) => e.message).join(", "),
error: parsed.error.issues.map((e) => e.message).join(", "),
rows: null,
columns: null,
stats: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import { useLocation } from "@remix-run/react";
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { conformZodMessage, parseWithZod } from "@conform-to/zod";
import { conformZodMessage, parseWithZod } from "@conform-to/zod/v4";
import { ExclamationTriangleIcon, FolderIcon, TrashIcon } from "@heroicons/react/20/solid";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { json } from "@remix-run/server-runtime";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import { Form, useActionData, useNavigation, useSearchParams } from "@remix-run/react";
import { json } from "@remix-run/server-runtime";
import React, { useCallback, useEffect, useRef, useState } from "react";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { parseWithZod } from "@conform-to/zod/v4";
import {
CheckCircleIcon,
RectangleStackIcon,
Expand Down Expand Up @@ -456,44 +456,40 @@ function StandardTaskForm({

const [showTemplateCreatedSuccessMessage, setShowTemplateCreatedSuccessMessage] = useState(false);

const [
form,
{
environmentId,
payload,
metadata,
taskIdentifier,
delaySeconds,
ttlSeconds,
idempotencyKey,
idempotencyKeyTTLSeconds,
queue,
concurrencyKey,
maxAttempts,
maxDurationSeconds,
triggerSource,
tags,
version,
machine,
region,
prioritySeconds,
},
] = useForm({
const [form, fields] = useForm({
id: "test-task",
// TODO: type this
lastResult: lastSubmission as any,
onSubmit(event, { formData }) {
event.preventDefault();

formData.set(payload.name, currentPayloadJson.current);
formData.set(metadata.name, currentMetadataJson.current);
formData.set(fields.payload.name, currentPayloadJson.current);
formData.set(fields.metadata.name, currentMetadataJson.current);

submit(formData, { method: "POST" });
},
onValidate({ formData }) {
return parseWithZod(formData, { schema: TestTaskData });
},
});
const {
environmentId,
taskIdentifier,
delaySeconds,
ttlSeconds,
idempotencyKey,
idempotencyKeyTTLSeconds,
queue,
concurrencyKey,
maxAttempts,
maxDurationSeconds,
triggerSource,
tags,
version,
machine,
region,
prioritySeconds,
} = fields;

return (
<Form className="flex h-full max-h-full flex-col" method="post" {...getFormProps(form)}>
Expand Down
Loading
Loading