Skip to content

[Improvement]: cron: beforeRun middleware — payload transform hook #14

@Arry8

Description

@Arry8

Summary

Extend beforeRun cron hooks to support payload transformation: hooks can return a payload patch that overrides fields for the current execution only, without mutating the stored job.

Problem

The existing beforeRun hook can only abort a job ({ abort: true }). There is no way to dynamically modify what a job executes at run time. Users who need runtime-variable behavior (inject today's date into a script arg, swap a model based on load, override cwd per environment) must either hardcode values at job creation or spin up a full agentTurn LLM session just to do string substitution — wasting tokens and introducing non-determinism.

A middleware-style beforeRun that returns a payload patch solves this cleanly: the hook receives the full CronHookContext (including ctx.payload) and can return { patch: CronPayloadPatch } to override specific fields for this run only. The stored job is never mutated.

Acceptance criteria

  • beforeRun hook may return { patch: CronPayloadPatch } to override payload fields for the current run
  • Patch is shallow-merged onto the stored payload (same semantics as cron.update payload patch)
  • Stored job payload is never mutated — patch applies only to the in-flight execution
  • Existing { abort: true, reason } return from beforeRun continues to work unchanged
  • Non-patch, non-abort return values from beforeRun are ignored (no-op, existing behavior)
  • ctx.payload on the hook context is the original stored payload (not the patched one)
  • Hook receives all three payload kinds: agentTurn, script, systemEvent
  • Patch validation: invalid patch shape is logged and skipped (hook error, job continues with original payload)
  • CronHookContext gains a payload field (CronPayload) so hooks can read the current payload
  • Unit tests: patch applied, abort still works, invalid patch ignored, stored job unchanged after run

Implementation plan

  1. Add payload: CronPayload to CronHookContext in src/cron/hooks.ts
  2. Add CronBeforeRunResult type: { abort: true; reason?: string } | { patch: CronPayloadPatch } (void/undefined = no-op)
  3. In runCronHooks (src/cron/hooks.ts): when hookPoint === "beforeRun" and result has patch, validate patch shape, merge with mergeCronPayload, accumulate into patchedPayload on the result
  4. Update CronHookRunResult to include patchedPayload?: CronPayload
  5. In runDueJob (src/cron/service/timer.ts): if beforeResult.patchedPayload is set, replace job.payload with patchedPayload before calling executeJobCoreWithTimeout — restore original payload afterward (finally block)
  6. Pass payload: job.payload into makeHookCtx so it appears on CronHookContext
  7. Add unit tests in src/cron/hooks.test.ts

Files affected

  • src/cron/hooks.ts (modify — add payload to context, handle patch result, update CronHookRunResult)
  • src/cron/service/timer.ts (modify — apply patchedPayload to job before executeJobCoreWithTimeout, restore after)
  • src/cron/hooks.test.ts (new — unit tests for patch, abort, invalid patch, no-op)

Additional notes

  • mergeCronPayload in src/cron/service/jobs.ts already implements the patch-merge semantics; reuse it
  • The "restore after" pattern in timer.ts mirrors how computeJobPreviousRunAtMs already saves/restores lastRunAtMs
  • Patch validation should reuse the gateway schema (CronPayloadPatchSchema) via ajv or inline kind-check — same guard as cron.update
  • No config schema change needed: CronHookEntry already accepts arbitrary return values; this is a behavioral extension only
  • This is upstream-worthy: pairs naturally with issue [Improvement]: cron: native script execution payload (bypass LLM for deterministic jobs) #11 (script payload) — a beforeRun hook can inject dynamic args into a script payload

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions