Skip to content

Feature Request: Allow schedules.task() to accept a schema option for typed manual .trigger() calls #3289

@mouguu

Description

@mouguu

Problem

schedules.task() and schemaTask() are mutually exclusive. If a task needs both schedule support (for cron/imperative schedules) and a typed custom payload (for manual .trigger() calls), users are forced into awkward workarounds.

Current situation

When using schedules.task(), the .trigger() method is typed to only accept ScheduledTaskPayload:

export const myTask = schedules.task({
  id: 'my-task',
  run: async (payload: ScheduledTaskPayload) => { ... }
});

// TypeScript error — can't pass custom fields
await myTask.trigger({ feedType: 'following', forcePush: true });
//                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Type '{ feedType: string; forcePush: boolean }' is not assignable to 'ScheduledTaskPayload'

What users do today

Option A — as any cast (dirty):

// In server route
await myTask.trigger({ feedType: 'following', forcePush: true } as any);

// In task run
run: async (rawPayload: any) => {
  const payload = MySchema.parse(rawPayload); // manual Zod parse, no type safety
}

Option B — two separate tasks (maintenance burden):

export const myScheduledTask = schedules.task({ id: 'my-task-scheduled', ... });
export const myManualTask = schemaTask({ id: 'my-task-manual', schema: MySchema, ... });

Neither option is good. Option A loses type safety entirely. Option B doubles maintenance surface.

Proposed Solution

Allow schedules.task() to accept an optional schema option. When present:

  • Manual .trigger() calls accept the schema's output type
  • The run function receives a merged payload: ScheduledTaskPayload & z.infer<typeof schema>
  • When triggered by a schedule (cron), schema fields fall back to Zod defaults
  • When triggered manually, schema fields come from the caller
const MySchema = z.object({
  feedType: z.enum(['for-you', 'following']).default('for-you'),
  forcePush: z.boolean().default(false),
});

export const myTask = schedules.task({
  id: 'my-task',
  schema: MySchema,  // optional
  run: async (payload: ScheduledTaskPayload & z.infer<typeof MySchema>) => {
    // feedType is always available — from caller or Zod default
    console.log(payload.feedType, payload.scheduleId);
  }
});

// Now fully typed, no `as any` needed
await myTask.trigger({ feedType: 'following', forcePush: true });

Why this matters

This is a common real-world pattern:

  • A scheduled task runs on a cron with sensible defaults
  • The same task can be triggered manually with overrides (e.g. forcePush: true, dryRun: true, different config)

The schedule and the schema are orthogonal concerns — there's no good reason they can't coexist.

Implementation notes

Since schema would be optional, this is non-breaking. Internally, when the task receives a ScheduledTaskPayload (from a cron trigger), the schema's .parse() with defaults would fill in missing fields. When triggered manually, the caller's payload is parsed through the schema normally.

The complexity is mainly in the TypeScript generics for the merged payload type — similar to what schemaTask already does.

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