Skip to content

bug: runs.retrieve() returns null for undefined fields on small inline outputs (superjson not fully deserialized) #3300

@isshaddad

Description

@isshaddad

Provide environment information

v4.4.3

Describe the bug

When a task returns an object that includes undefined for optional fields, runs.retrieve() can return those fields as null if the output is small enough to be returned inline from the API.

Expected: values round-trip like a full superjson deserialize (typically undefined for optional fields), so z.number().optional) still works.

Actual: inline responses can return superjson’s JSON “shape” where undefined becomes null.

Large/offloaded outputs (presigned blob path) deserialize correctly and return undefined.

It's most easily with undefined coming back as null, but anything superjson normally preserves (Date, Map, Set, BigInt, RegExp, etc.) is also wrong on small inline outputs, big outputs fetched via presign still look fine.

Reproduction repo

n/a

To reproduce

  1. Add tasks:
import { schemaTask } from "@trigger.dev/sdk";
import { z } from "zod";

export const smallOptionalUndefined = schemaTask({
  id: "small-optional-undefined",
  schema: z.object({}),
  run: async () => ({ value: 1, optional: undefined as undefined | number }),
});

export const bigOptionalUndefined = schemaTask({
  id: "big-optional-undefined",
  schema: z.object({}),
  run: async () => ({
    value: 1,
    optional: undefined as undefined | number,
    filler: "x".repeat(200 * 1024),
  }),
});

2 . Start the dev server:
npx trigger.dev dev
3. Trigger and retrieve:

import { runs, tasks } from "@trigger.dev/sdk";

async function waitForRun(id: string) {
  while (true) {
    const run = await runs.retrieve(id);
    if (run.status === "COMPLETED") return run;
    if (run.status === "FAILED" || run.status === "CANCELED") {
      throw new Error(`Run ${id} ended with status ${run.status}`);
    }
    await new Promise((r) => setTimeout(r, 1000));
  }
}

const small = await waitForRun((await tasks.trigger("small-optional-undefined", {})).id);
const big = await waitForRun((await tasks.trigger("big-optional-undefined", {})).id);

console.log("small.optional =", (small.output as any)?.optional); // observed: null
console.log("big.optional   =", (big.output as any)?.optional);   // observed: undefined

Expected:
small.optional = undefined
big.optional = undefined

Observed:
small.optional = null
big.optional = undefined

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions