Skip to content

Commit dbe6470

Browse files
committed
cron: expose payload on CronHookContext for middleware hooks
beforeRun/afterComplete/onFailure/afterRun hooks now receive ctx.payload so scripts can inspect kind, command, message, etc. and make decisions (abort, conditional logic, audit) without guessing from the job name. - Add payload: CronPayload to CronHookContext type (hooks.ts) - Pass structuredClone(job.payload) in all three makeHookCtx sites (timer.ts scheduled runs, timer.ts catch-up runs, ops.ts manual runs) - hooks.test.ts: 3 new tests — kind-based abort, non-matching kind, payload field access via ctx.meta
1 parent 8f13017 commit dbe6470

4 files changed

Lines changed: 48 additions & 1 deletion

File tree

src/cron/hooks.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ beforeEach(() => {
3535

3636
function makeCtx(
3737
hookPoint: "beforeRun" | "afterComplete" | "onFailure" | "afterRun",
38+
payload: CronHookContext["payload"] = { kind: "agentTurn", message: "hello" },
3839
): CronHookContext {
3940
return {
4041
hookPoint,
@@ -45,6 +46,7 @@ function makeCtx(
4546
agentId: "test-agent",
4647
schedule: { kind: "every", everyMs: 60_000 },
4748
},
49+
payload,
4850
meta: {},
4951
log: noopLog,
5052
};
@@ -320,4 +322,44 @@ describe("runCronHooks", () => {
320322
const result = await runCronHooks("afterRun", makeCtx("afterRun"), entries);
321323
expect(result.aborted).toBe(false);
322324
});
325+
326+
it("exposes payload.kind to hook via ctx.payload", async () => {
327+
// Hook reads ctx.payload.kind and aborts only when it is "agentTurn".
328+
const script = inlineHook(
329+
`async function(ctx) { return ctx.payload.kind === "agentTurn" ? { abort: true, reason: "kind-check" } : {}; }`,
330+
);
331+
const entries = [{ script, priority: 10 }];
332+
const result = await runCronHooks(
333+
"beforeRun",
334+
makeCtx("beforeRun", { kind: "agentTurn", message: "hi" }),
335+
entries,
336+
);
337+
expect(result.aborted).toBe(true);
338+
expect(result.reason).toBe("kind-check");
339+
});
340+
341+
it("does not abort when payload.kind does not match hook condition", async () => {
342+
const script = inlineHook(
343+
`async function(ctx) { return ctx.payload.kind === "agentTurn" ? { abort: true } : {}; }`,
344+
);
345+
const entries = [{ script, priority: 10 }];
346+
// systemEvent payload — hook condition should not trigger abort.
347+
const result = await runCronHooks(
348+
"beforeRun",
349+
makeCtx("beforeRun", { kind: "systemEvent", text: "ping" }),
350+
entries,
351+
);
352+
expect(result.aborted).toBe(false);
353+
});
354+
355+
it("exposes all payload fields to the hook", async () => {
356+
// Hook reads nested payload fields and signals via meta to verify they are accessible.
357+
const script = inlineHook(
358+
`async function(ctx) { ctx.meta.seenMessage = ctx.payload.message; }`,
359+
);
360+
const entries = [{ script, priority: 10 }];
361+
const ctx = makeCtx("afterRun", { kind: "agentTurn", message: "check-fields" });
362+
await runCronHooks("afterRun", ctx, entries);
363+
expect(ctx.meta.seenMessage).toBe("check-fields");
364+
});
323365
});

src/cron/hooks.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { CronConfig, CronHookEntry, CronLifecycleHookPoint } from "../confi
44
import { importFileModule, resolveFunctionModuleExport } from "../hooks/module-loader.js";
55
import { resolveUserPath } from "../utils.js";
66
import type { Logger } from "./service/state.js";
7-
import type { CronJob } from "./types.js";
7+
import type { CronJob, CronPayload } from "./types.js";
88

99
const DEFAULT_PRIORITY = 10;
1010
const HOOK_TIMEOUT_MS = 10_000;
@@ -13,6 +13,8 @@ export type CronHookContext = {
1313
hookPoint: CronLifecycleHookPoint;
1414
workflow: string;
1515
job: Pick<CronJob, "id" | "name" | "agentId" | "schedule">;
16+
/** The job's payload for this run. Hook scripts can inspect kind/command/message to make decisions. */
17+
payload: CronPayload;
1618
error?: string;
1719
status?: string;
1820
durationMs?: number;

src/cron/service/ops.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ async function finishPreparedManualRun(
478478
agentId: executionJob.agentId,
479479
schedule: executionJob.schedule,
480480
},
481+
payload: structuredClone(executionJob.payload),
481482
meta: hookMeta,
482483
log: state.deps.log,
483484
basePath: hookBasePath,

src/cron/service/timer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,7 @@ export async function onTimer(state: CronServiceState) {
645645
hookPoint,
646646
workflow: "cron",
647647
job: { id: job.id, name: job.name, agentId: job.agentId, schedule: job.schedule },
648+
payload: structuredClone(job.payload),
648649
meta: hookMeta,
649650
log: state.deps.log,
650651
basePath: hookBasePath,
@@ -1027,6 +1028,7 @@ async function runStartupCatchupCandidate(
10271028
hookPoint,
10281029
workflow: "cron",
10291030
job: { id: job.id, name: job.name, agentId: job.agentId, schedule: job.schedule },
1031+
payload: structuredClone(job.payload),
10301032
meta: hookMeta,
10311033
log: state.deps.log,
10321034
basePath: hookBasePath,

0 commit comments

Comments
 (0)