Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
be5600a
chore: upgrade to zod v4
carderne Jun 30, 2026
040a696
format, typecheck, fix build
carderne Jun 30, 2026
bce98b0
fix failing test
carderne Jun 30, 2026
6ccd793
format, lint
carderne Jul 3, 2026
9e86434
cli dep zod4, update changelog
carderne Jul 3, 2026
eca97c6
bump to platform@1.2.0
carderne Jul 3, 2026
d2df830
updates
carderne Aug 3, 2026
f32bb3a
fixes
carderne Aug 3, 2026
daf5a44
fix test
carderne Aug 3, 2026
31d5262
bump platform dep
carderne Aug 3, 2026
71a966b
fix schema
carderne Aug 3, 2026
77c3969
lint
carderne Aug 3, 2026
5497803
fix: preserve zod v4 configuration behavior
carderne Aug 3, 2026
3b37b21
fix: declare zod peer dependencies
carderne Aug 4, 2026
fbd1280
fix: use zod-error v2 with zod 4
carderne Aug 4, 2026
d4f2231
fix: preserve empty task payloads and validation messages
carderne Aug 4, 2026
9e36aa9
improve cross-version zod support
carderne Aug 4, 2026
c258d6a
fix ci
carderne Aug 4, 2026
872f95e
verify omitted payload
carderne Aug 4, 2026
97183a4
fix(core): preserve message payloads during validation
carderne Aug 4, 2026
696306a
fix: reconcile Zod 4 migration with current schemas
carderne Sep 3, 2026
89483d1
fix: stabilize alert form reset
carderne Sep 3, 2026
3c075ce
chore: bump runtime Zod to 4.5.4
carderne Sep 3, 2026
7f6e17a
fix(core): keep composed schemas on Zod 4
carderne Sep 3, 2026
c172490
test(core): assert compatibility bundle output exists
carderne Sep 3, 2026
e945c1f
improve zod v3 compat
carderne Sep 7, 2026
6ddce06
small fix
carderne Sep 7, 2026
ed4f866
fix(cli): explain unsupported Zod versions
carderne Sep 7, 2026
eec305e
fix(core): infer Zod tool payloads without the optional ai package
carderne Sep 7, 2026
e9a9c58
fix: support Zod 3.25.56 alongside Zod 4
carderne Sep 8, 2026
a4b1d62
test(core): use explicit Zod compatibility module paths
carderne Sep 8, 2026
cc90598
test: refresh prompt prefix snapshot for Zod 4
carderne Sep 8, 2026
a689396
fix agents.md
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
---

Add Zod 4 support. You can now use Zod 3.25.56+ 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. 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