Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions nodes/workflow-monitor.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,20 @@

### Outputs

1. Status updates (active workflows)
: payload (object) : The workflow details from the API, sent on every status poll while workflow is active.
1. All status updates
: payload (object) : The workflow details from the API, sent on **every** status poll including terminal states. Use this output for tracking/monitoring all workflow state changes.
: workflowId (string) : The ID of the workflow.

2. Success
: payload (object) : The workflow details from the API, sent once when the workflow completes successfully.
: payload (object) : The workflow details from the API, sent once when the workflow completes successfully. Use this output for flow control to trigger downstream success actions.
: workflowId (string) : The ID of the workflow.

3. Error
: payload (object) : The workflow details from the API, sent once when the workflow fails or is cancelled.
: payload (object) : The workflow details from the API, sent once when the workflow fails or is cancelled. Use this output for flow control to trigger downstream error handling.
: workflowId (string) : The ID of the workflow.

**Note:** When a workflow succeeds, both outputs 1 and 2 fire. When a workflow fails/cancels, both outputs 1 and 3 fire. This allows output 1 to be used for comprehensive status tracking while outputs 2 and 3 can be used for flow control.

### Details

This node queries the Seqera Platform API for the current status of a workflow run. When *Keep polling status* is enabled (default) the node will keep polling at the configured interval until the workflow reaches a terminal state (succeeded, failed, cancelled, or unknown).
Expand Down
13 changes: 7 additions & 6 deletions nodes/workflow-monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,17 @@ module.exports = function (RED) {
};

// Decide which output to send to
// Output 1: Active (submitted, running)
// Output 2: Succeeded
// Output 3: Failed/Cancelled/Unknown
// Output 1: All status updates (always fires for tracking/monitoring)
// Output 2: Succeeded (terminal state, for flow control)
// Output 3: Failed/Cancelled/Unknown (terminal state, for flow control)
if (/^(submitted|running)$/.test(statusLower)) {
send([outMsg, null, null]);
} else if (/^(succeeded)$/.test(statusLower)) {
send([null, outMsg, null]);
// Send to both output 1 (status tracking) and output 2 (success flow)
send([outMsg, outMsg, null]);
} else {
// failed, cancelled, unknown
send([null, null, outMsg]);
// failed, cancelled, unknown - send to both output 1 (status tracking) and output 3 (failure flow)
send([outMsg, null, outMsg]);
}

// If keepPolling disabled OR workflow reached a final state, stop polling THIS workflow
Expand Down
198 changes: 198 additions & 0 deletions test/workflow-monitor_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,204 @@ describe("seqera-workflow-monitor Node", function () {
monitorNode.receive({ workflowId: "wf-123" });
});
});

it("should send to BOTH output 1 and output 2 when status is succeeded", function (done) {
const flow = [
createConfigNode(),
{
id: "monitor1",
type: "seqera-workflow-monitor",
name: "Test Monitor",
seqera: "config-node-1",
workflowId: "workflowId",
workflowIdType: "msg",
keepPolling: false,
wires: [["helper1"], ["helper2"], ["helper3"]],
},
{ id: "helper1", type: "helper" },
{ id: "helper2", type: "helper" },
{ id: "helper3", type: "helper" },
];

nock(DEFAULT_BASE_URL)
.get("/workflow/wf-123")
.query(true)
.reply(200, createWorkflowResponse({ status: "succeeded" }));

helper.load([configNode, workflowMonitorNode], flow, createCredentials(), function () {
const monitorNode = helper.getNode("monitor1");
const helper1 = helper.getNode("helper1");
const helper2 = helper.getNode("helper2");
const helper3 = helper.getNode("helper3");

let output1Received = false;
let output2Received = false;

const checkDone = () => {
if (output1Received && output2Received) {
done();
}
};

helper1.on("input", function (msg) {
try {
expect(msg.payload.workflow.status).to.equal("succeeded");
output1Received = true;
checkDone();
} catch (err) {
done(err);
}
});

helper2.on("input", function (msg) {
try {
expect(msg.payload.workflow.status).to.equal("succeeded");
output2Received = true;
checkDone();
} catch (err) {
done(err);
}
});

helper3.on("input", function () {
done(new Error("Should not receive on output 3 for succeeded status"));
});

monitorNode.receive({ workflowId: "wf-123" });
});
});

it("should send to BOTH output 1 and output 3 when status is failed", function (done) {
const flow = [
createConfigNode(),
{
id: "monitor1",
type: "seqera-workflow-monitor",
name: "Test Monitor",
seqera: "config-node-1",
workflowId: "workflowId",
workflowIdType: "msg",
keepPolling: false,
wires: [["helper1"], ["helper2"], ["helper3"]],
},
{ id: "helper1", type: "helper" },
{ id: "helper2", type: "helper" },
{ id: "helper3", type: "helper" },
];

nock(DEFAULT_BASE_URL)
.get("/workflow/wf-123")
.query(true)
.reply(200, createWorkflowResponse({ status: "failed" }));

helper.load([configNode, workflowMonitorNode], flow, createCredentials(), function () {
const monitorNode = helper.getNode("monitor1");
const helper1 = helper.getNode("helper1");
const helper2 = helper.getNode("helper2");
const helper3 = helper.getNode("helper3");

let output1Received = false;
let output3Received = false;

const checkDone = () => {
if (output1Received && output3Received) {
done();
}
};

helper1.on("input", function (msg) {
try {
expect(msg.payload.workflow.status).to.equal("failed");
output1Received = true;
checkDone();
} catch (err) {
done(err);
}
});

helper2.on("input", function () {
done(new Error("Should not receive on output 2 for failed status"));
});

helper3.on("input", function (msg) {
try {
expect(msg.payload.workflow.status).to.equal("failed");
output3Received = true;
checkDone();
} catch (err) {
done(err);
}
});

monitorNode.receive({ workflowId: "wf-123" });
});
});

it("should send to BOTH output 1 and output 3 when status is cancelled", function (done) {
const flow = [
createConfigNode(),
{
id: "monitor1",
type: "seqera-workflow-monitor",
name: "Test Monitor",
seqera: "config-node-1",
workflowId: "workflowId",
workflowIdType: "msg",
keepPolling: false,
wires: [["helper1"], ["helper2"], ["helper3"]],
},
{ id: "helper1", type: "helper" },
{ id: "helper2", type: "helper" },
{ id: "helper3", type: "helper" },
];

nock(DEFAULT_BASE_URL)
.get("/workflow/wf-123")
.query(true)
.reply(200, createWorkflowResponse({ status: "cancelled" }));

helper.load([configNode, workflowMonitorNode], flow, createCredentials(), function () {
const monitorNode = helper.getNode("monitor1");
const helper1 = helper.getNode("helper1");
const helper2 = helper.getNode("helper2");
const helper3 = helper.getNode("helper3");

let output1Received = false;
let output3Received = false;

const checkDone = () => {
if (output1Received && output3Received) {
done();
}
};

helper1.on("input", function (msg) {
try {
expect(msg.payload.workflow.status).to.equal("cancelled");
output1Received = true;
checkDone();
} catch (err) {
done(err);
}
});

helper2.on("input", function () {
done(new Error("Should not receive on output 2 for cancelled status"));
});

helper3.on("input", function (msg) {
try {
expect(msg.payload.workflow.status).to.equal("cancelled");
output3Received = true;
checkDone();
} catch (err) {
done(err);
}
});

monitorNode.receive({ workflowId: "wf-123" });
});
});
});

describe("polling behavior", function () {
Expand Down