diff --git a/.changeset/fn-7224-workflow-notifications.md b/.changeset/fn-7224-workflow-notifications.md new file mode 100644 index 0000000000..0964259570 --- /dev/null +++ b/.changeset/fn-7224-workflow-notifications.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Send ntfy notifications from workflow graph lifecycle and notify-node flows. +category: fix +dev: Workflow column transitions now use moveTask events; workflow-notify is enabled in default ntfy events. diff --git a/packages/engine/src/__tests__/ce-workflow-step-executor.test.ts b/packages/engine/src/__tests__/ce-workflow-step-executor.test.ts index 97d4b227a8..85ff33b7e3 100644 --- a/packages/engine/src/__tests__/ce-workflow-step-executor.test.ts +++ b/packages/engine/src/__tests__/ce-workflow-step-executor.test.ts @@ -314,6 +314,47 @@ describe("CE workflow-step executor integration", () => { expect(live.mergeDetails?.mergeConfirmed).toBe(true); }); + it("uses moveTask for workflow graph column transitions so lifecycle notifications fire", async () => { + const store = createMockStore(); + store.getTask.mockResolvedValue(baseStepTask({ column: "todo" }) as any); + const { executor } = makeExecutor(store); + const settings = await store.getSettings(); + const primitives = (executor as any).createAuthoritativeWorkflowPrimitives(settings); + + const result = await primitives.transitionTask( + { + run: { runId: "run-1", taskId: "FN-CE-1", workflowId: "builtin:coding" }, + node: { node: { id: "schedule", kind: "prompt", column: "todo", config: {} }, context: {} }, + }, + baseStepTask({ column: "todo" }), + { + column: "in-progress", + status: "queued", + reason: "workflow-schedule", + preserveProgress: true, + }, + ); + + expect(result).toEqual({ outcome: "success", value: "workflow-schedule" }); + expect(store.moveTask).toHaveBeenCalledWith( + "FN-CE-1", + "in-progress", + expect.objectContaining({ + moveSource: "engine", + preserveProgress: true, + workflowMoveSource: "workflow-graph", + workflowMoveMetadata: expect.objectContaining({ + reason: "workflow-schedule", + nodeId: "schedule", + workflowId: "builtin:coding", + runId: "run-1", + }), + }), + ); + expect(store.updateTask).toHaveBeenCalledWith("FN-CE-1", { status: "queued" }); + expect(store.updateTask).not.toHaveBeenCalledWith("FN-CE-1", expect.objectContaining({ column: "in-progress" })); + }); + it("treats terminal graph step projection as success when the legacy pass rejects", async () => { const store = createMockStore(); store.getTask.mockResolvedValue(baseStepTask({ diff --git a/packages/engine/src/__tests__/notifier.test.ts b/packages/engine/src/__tests__/notifier.test.ts index 93f0014f9b..ff48a3eecf 100644 --- a/packages/engine/src/__tests__/notifier.test.ts +++ b/packages/engine/src/__tests__/notifier.test.ts @@ -26,6 +26,7 @@ describe("Ntfy notifier helpers", () => { expect(DEFAULT_NTFY_EVENTS).toContain("message:agent-to-user"); expect(DEFAULT_NTFY_EVENTS).toContain("message:agent-to-agent"); expect(DEFAULT_NTFY_EVENTS).toContain("message:room"); + expect(DEFAULT_NTFY_EVENTS).toContain("workflow-notify"); }); it("checks awaiting-input event enablement", () => { diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 081c74ac2e..fa2b7e7759 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -5727,11 +5727,36 @@ export class TaskExecutor { return { outcome: "success", value: "steps-updated", data: { count: steps.length } }; }, transitionTask: async (_ctx, task, input) => { + const taskStore = this.store; const patch: Partial = {}; - if (input.column !== undefined) patch.column = input.column; + /* + FNXC:WorkflowNotifications 2026-06-29-08:50: + Workflow graph lifecycle transitions must use TaskStore move semantics, not raw `updateTask({ column })`, because ntfy/webhook notification delivery is subscribed to `task:moved`. Direct column writes make graph-owned tasks invisible to in-review/done lifecycle notifications and bypass column hooks. + */ + if (input.column !== undefined) { + const moveOptions = { + preserveProgress: input.preserveProgress, + moveSource: "engine" as const, + workflowMoveSource: "workflow-graph", + workflowMoveMetadata: { + reason: input.reason, + nodeId: _ctx.node.node.id, + workflowId: _ctx.run.workflowId, + runId: _ctx.run.runId, + }, + }; + const storeWithMove = taskStore as typeof taskStore & { + moveTask?: typeof taskStore.moveTask; + }; + if (typeof storeWithMove.moveTask === "function") { + await storeWithMove.moveTask(task.id, input.column, moveOptions); + } else { + patch.column = input.column; + } + } if (input.status !== undefined && input.status !== null) patch.status = input.status; if (Object.keys(patch).length > 0) { - await this.store.updateTask(task.id, patch); + await taskStore.updateTask(task.id, patch); } return { outcome: "success", value: input.reason }; }, diff --git a/packages/engine/src/notifier.ts b/packages/engine/src/notifier.ts index ff986b67a6..c6059f141c 100644 --- a/packages/engine/src/notifier.ts +++ b/packages/engine/src/notifier.ts @@ -36,6 +36,7 @@ export const DEFAULT_NTFY_EVENTS: readonly NtfyNotificationEvent[] = [ "db-corruption-detected", "fallback-used", "token-budget", + "workflow-notify", "message:agent-to-user", "message:agent-to-agent", "message:room",