Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a4c9426
chore: upgrade to zod v4
carderne Jun 30, 2026
e251fa3
format, typecheck, fix build
carderne Jun 30, 2026
bc9f672
fix failing test
carderne Jun 30, 2026
e6067c6
format, lint
carderne Jul 3, 2026
32f9e87
cli dep zod4, update changelog
carderne Jul 3, 2026
46a47fb
bump to platform@1.2.0
carderne Jul 3, 2026
5f71d4f
updates
carderne Aug 3, 2026
e875e2c
fixes
carderne Aug 3, 2026
0f64144
fix test
carderne Aug 3, 2026
47f6a7e
bump platform dep
carderne Aug 3, 2026
63e909f
fix schema
carderne Aug 3, 2026
388b8d1
lint
carderne Aug 3, 2026
bbb9ba2
fix: preserve zod v4 configuration behavior
carderne Aug 3, 2026
bc21f15
fix: declare zod peer dependencies
carderne Aug 4, 2026
fea252f
fix: use zod-error v2 with zod 4
carderne Aug 4, 2026
eed59da
fix: preserve empty task payloads and validation messages
carderne Aug 4, 2026
75dbe0f
improve cross-version zod support
carderne Aug 4, 2026
ae93035
fix ci
carderne Aug 4, 2026
574c94c
verify omitted payload
carderne Aug 4, 2026
ed21fe8
fix(core): preserve message payloads during validation
carderne Aug 4, 2026
a5fccbe
fix: reconcile Zod 4 migration with current schemas
carderne Sep 3, 2026
cd8b3c7
fix: stabilize alert form reset
carderne Sep 3, 2026
6774f6c
chore: bump runtime Zod to 4.5.4
carderne Sep 3, 2026
c13adc6
fix(core): keep composed schemas on Zod 4
carderne Sep 3, 2026
c81650d
test(core): assert compatibility bundle output exists
carderne Sep 3, 2026
9d88bca
improve zod v3 compat
carderne Sep 7, 2026
1154b4b
small fix
carderne Sep 7, 2026
4b80a3d
fix(cli): explain unsupported Zod versions
carderne Sep 7, 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
---

Add Zod 4 support. You can now use Zod 3.25.76+ or Zod 4.

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 (currently `4.4.3`). 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