You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
Add payload: CronPayload to CronHookContext in src/cron/hooks.ts
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
Update CronHookRunResult to include patchedPayload?: CronPayload
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)
Pass payload: job.payload into makeHookCtx so it appears on CronHookContext
Summary
Extend
beforeRuncron 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
beforeRunhook 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
beforeRunthat returns a payload patch solves this cleanly: the hook receives the fullCronHookContext(includingctx.payload) and can return{ patch: CronPayloadPatch }to override specific fields for this run only. The stored job is never mutated.Acceptance criteria
beforeRunhook may return{ patch: CronPayloadPatch }to override payload fields for the current runcron.updatepayload patch){ abort: true, reason }return frombeforeRuncontinues to work unchangedbeforeRunare ignored (no-op, existing behavior)ctx.payloadon the hook context is the original stored payload (not the patched one)agentTurn,script,systemEventCronHookContextgains apayloadfield (CronPayload) so hooks can read the current payloadImplementation plan
payload: CronPayloadtoCronHookContextinsrc/cron/hooks.tsCronBeforeRunResulttype:{ abort: true; reason?: string } | { patch: CronPayloadPatch }(void/undefined = no-op)runCronHooks(src/cron/hooks.ts): whenhookPoint === "beforeRun"and result haspatch, validate patch shape, merge withmergeCronPayload, accumulate intopatchedPayloadon the resultCronHookRunResultto includepatchedPayload?: CronPayloadrunDueJob(src/cron/service/timer.ts): ifbeforeResult.patchedPayloadis set, replacejob.payloadwithpatchedPayloadbefore callingexecuteJobCoreWithTimeout— restore original payload afterward (finally block)payload: job.payloadintomakeHookCtxso it appears onCronHookContextsrc/cron/hooks.test.tsFiles 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
mergeCronPayloadinsrc/cron/service/jobs.tsalready implements the patch-merge semantics; reuse itcomputeJobPreviousRunAtMsalready saves/restoreslastRunAtMsCronPayloadPatchSchema) viaajvor inline kind-check — same guard ascron.updateCronHookEntryalready accepts arbitrary return values; this is a behavioral extension onlybeforeRunhook can inject dynamic args into a script payload