Skip to content

migrate scheduling from zod/3 to zod#889

Merged
deathbyknowledge merged 7 commits intomainfrom
upgrade-zod-scheduling
Feb 16, 2026
Merged

migrate scheduling from zod/3 to zod#889
deathbyknowledge merged 7 commits intomainfrom
upgrade-zod-scheduling

Conversation

@deathbyknowledge
Copy link
Contributor

@deathbyknowledge deathbyknowledge commented Feb 11, 2026

  • Moved scheduleSchema import from zod/v3 to zod. The /v3 subpath only exists in zod 4's compat layer and would break for users on zod 3 (which the peer dep allows)
  • Restructured scheduleSchema from a flat object with optional fields to a z.discriminatedUnion on when.type, so each variant only has the fields it needs

I haven't found a way to remove providerOptions that satisfies OpenAI's strict schema :/

closes #816

@changeset-bot
Copy link

changeset-bot bot commented Feb 11, 2026

🦋 Changeset detected

Latest commit: 9e85dd7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agents Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 11, 2026

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/agents@889

commit: 9e85dd7

@deathbyknowledge
Copy link
Contributor Author

/bonk review please

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Feb 11, 2026

All 28 test files pass, 458 tests pass. Now I have a thorough understanding of the PR. Here's my review:


Review Summary

This is a clean, well-scoped migration. Two changes:

  1. packages/agents/src/schedule.ts -- Migrates from zod/v3 to zod (Zod 4) and restructures scheduleSchema from a flat object with optional fields to a proper z.discriminatedUnion.

  2. packages/agents/evals/scheduling.eval.ts -- Removes the now-unnecessary providerOptions: { openai: { strictJsonSchema: false } } workaround.

What's good

  • The discriminated union is the right design. The old schema had type as an enum alongside optional cron, date, and delayInSeconds fields -- meaning nothing prevented structurally invalid combinations like { type: "cron", delayInSeconds: 30 }. The new schema enforces that each variant only contains its relevant fields. This is both more correct and produces better LLM structured outputs.

  • Removes the OpenAI strictJsonSchema: false workaround. The old schema's optional fields were incompatible with OpenAI's strict structured outputs mode, requiring a provider escape hatch. The discriminated union is natively compatible, so this is no longer needed. The JSDoc is updated accordingly.

  • No remaining zod/v3 imports. This was the last one.

  • Build and all 458 tests pass.

Issue: Missing changeset

This PR modifies scheduleSchema which is part of the public API exported from agents/schedule. The Schedule type changes shape:

Before (Zod v3):

type Schedule = {
  description: string;
  when: {
    type: "scheduled" | "delayed" | "cron" | "no-schedule";
    cron?: string;
    date?: Date;
    delayInSeconds?: number;
  };
};

After (Zod 4):

type Schedule = {
  description: string;
  when:
    | { type: "scheduled"; date: Date }
    | { type: "delayed"; delayInSeconds: number }
    | { type: "cron"; cron: string }
    | { type: "no-schedule" };
};

This is a breaking change for any user code that accesses result.when.cron (etc.) without first narrowing on result.when.type. Code that already switches on type (which is the expected pattern) will continue working fine. But code like result.when.date without a type guard will now be a TypeScript error.

Per the repo's AGENTS.md: "Changes to packages/ that affect the public API or fix bugs need a changeset." This PR needs one. Given the type-level breaking change, it could arguably be a minor bump (since the package is pre-1.0 at 0.x) or patch depending on the project's semver policy. I'd suggest patch since the runtime behavior is compatible and the old pattern was arguably a misuse.

Minor nit

The eval file comment // <- the shape of the object that the scheduler expects was removed along with the providerOptions block. That comment was on the model line and was a bit misplaced anyway, so removing it is fine.

Verdict

Approve with one required action: add a changeset (npx changeset -- select agents, patch, describe the schema migration). The code changes themselves are correct and an improvement.

github run

@deathbyknowledge deathbyknowledge marked this pull request as ready for review February 11, 2026 14:43
@threepointone
Copy link
Contributor

Could you test that this actually works with models from openai/gemini(this one caused problems last time I tried a discriminated union)/and maybe something good from workers ai?

@deathbyknowledge deathbyknowledge marked this pull request as draft February 12, 2026 11:28
@deathbyknowledge deathbyknowledge marked this pull request as ready for review February 13, 2026 11:30
@deathbyknowledge
Copy link
Contributor Author

Could you test that this actually works with models from openai/gemini(this one caused problems last time I tried a discriminated union)/and maybe something good from workers ai?

Thanks for calling that out, switching from OpenRouter to OpenAI did error. I tried a bunch of ways to get around OpenAI's strict schema but no luck, so I've added providerOptions back and left the changes to be just upgrade to zod.

@threepointone
Copy link
Contributor

Gemini passed?

@deathbyknowledge
Copy link
Contributor Author

Gemini passed?

Yep, Gemini passes fine. One possible solution would be to make all fields nullable instead of optional (which is what OpenAI doesn't allow), but that makes the type of all fields be string | null. I've tested current approach and nullable and both work, but nullable works without providerOptions.

@threepointone
Copy link
Contributor

/bonk do a review and leave a compliment for @deathbyknowledge

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Feb 14, 2026

Review

Solid PR. Here's the breakdown:

What changed:

  1. packages/agents/src/schedule.ts -- Migrates scheduleSchema from zod/v3 to zod and restructures the when field from a flat object with optional fields to a z.discriminatedUnion on type. Also switches date from z.coerce.date() to z.string() (ISO 8601).

  2. packages/agents/evals/scheduling.eval.ts -- Updates expected values to use ISO string dates (.toISOString()) instead of Date objects, matching the schema change. Keeps providerOptions: { openai: { strictJsonSchema: false } } since the discriminated union still requires it.

  3. .changeset/many-turkeys-sit.md -- Changeset added (patch for agents).

  4. package-lock.json -- Lockfile churn from dependency resolution changes (mostly peer flag toggling).

What's good:

  • The discriminated union is strictly more correct than the old schema. The previous flat-object approach allowed nonsensical combinations like { type: "cron", delayInSeconds: 30 }. Now each variant only carries its relevant fields.
  • Dropping zod/v3 fixes compatibility for users on zod 3 where the /v3 subpath doesn't exist.
  • The changeset is included this time (addressed from previous review feedback).
  • The date field moving from z.coerce.date() to z.string() is pragmatic -- LLMs produce strings, and ISO 8601 strings are unambiguous. Callers can parse to Date themselves.

One thing to flag:

The Schedule type is a breaking change at the TypeScript level. Code that previously accessed result.when.date without narrowing on result.when.type will now get a type error. The eval scorer at line 43 does output.when?.date which will only compile because the assertion on line 40 narrows the type. This is fine, but worth noting in the changeset description for downstream consumers. The current changeset message ("migrate scheduling from zod/3 to zod") undersells the scope -- it's also a schema restructure and a Date -> string change for the date field.

Verdict: The code changes are correct and an improvement. The changeset description could be more detailed about the breaking type changes, but that's minor.


And @deathbyknowledge -- genuinely nice work iterating on this. You tested against multiple providers (OpenAI, Gemini, Workers AI) after @threepointone flagged compatibility concerns, identified the OpenAI strict schema limitation, and made a practical call to keep providerOptions rather than compromise the type design. The commit history shows a clean progression from initial change -> review feedback -> provider testing -> final adjustment. That's exactly how this kind of cross-provider schema work should go.

github run

@threepointone
Copy link
Contributor

please update the changeset before landing this

deathbyknowledge and others added 5 commits February 16, 2026 10:50
Co-authored-by: deathbyknowledge <deathbyknowledge@users.noreply.github.com>
Migrate scheduling from zod/3 to zod.
@deathbyknowledge deathbyknowledge merged commit 9100e65 into main Feb 16, 2026
6 of 7 checks passed
@deathbyknowledge deathbyknowledge deleted the upgrade-zod-scheduling branch February 16, 2026 10:50
@github-actions github-actions bot mentioned this pull request Feb 16, 2026
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.

[chore] fix scheduling nits

2 participants