Skip to content

Commit 43dc952

Browse files
committed
fix: record NODE_FAILED when child workflow throws
- Wrap child.result() Promise.race in try-catch to handle rejections - Record NODE_FAILED trace event before rethrowing so UI shows node as failed instead of stuck running - Fix child.cancel() to use getExternalWorkflowHandle (ChildWorkflowHandle doesn't have cancel method) - Fix drizzle and() type error in listChildren by using array-based conditions pattern Signed-off-by: betterclever <[email protected]>
1 parent 0dc61a7 commit 43dc952

File tree

2 files changed

+37
-9
lines changed

2 files changed

+37
-9
lines changed

backend/src/workflows/repository/workflow-run.repository.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Inject, Injectable } from '@nestjs/common';
2-
import { and, desc, eq, sql } from 'drizzle-orm';
2+
import { and, desc, eq, sql, type SQL } from 'drizzle-orm';
33
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
44

55
import { DRIZZLE_TOKEN } from '../../database/database.module';
@@ -141,15 +141,15 @@ export class WorkflowRunRepository {
141141
parentRunId: string,
142142
options: { organizationId?: string | null; limit?: number } = {},
143143
): Promise<WorkflowRunRecord[]> {
144-
let condition = eq(workflowRunsTable.parentRunId, parentRunId);
144+
const conditions: SQL[] = [eq(workflowRunsTable.parentRunId, parentRunId)];
145145
if (options.organizationId) {
146-
condition = and(condition, eq(workflowRunsTable.organizationId, options.organizationId));
146+
conditions.push(eq(workflowRunsTable.organizationId, options.organizationId));
147147
}
148148

149149
return this.db
150150
.select()
151151
.from(workflowRunsTable)
152-
.where(condition)
152+
.where(and(...conditions))
153153
.orderBy(desc(workflowRunsTable.createdAt))
154154
.limit(options.limit ?? 200);
155155
}

worker/src/temporal/workflows/index.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
ApplicationFailure,
33
condition,
4+
getExternalWorkflowHandle,
45
proxyActivities,
56
setHandler,
67
startChild,
@@ -356,13 +357,40 @@ export async function shipsecWorkflowRun(
356357
})
357358

358359
const timeoutMs = timeoutSeconds * 1000
359-
const outcome = await Promise.race([
360-
child.result().then((result) => ({ kind: 'result' as const, result })),
361-
sleep(timeoutMs).then(() => ({ kind: 'timeout' as const })),
362-
])
360+
let outcome: { kind: 'result'; result: Awaited<ReturnType<typeof child.result>> } | { kind: 'timeout' }
361+
try {
362+
outcome = await Promise.race([
363+
child.result().then((result) => ({ kind: 'result' as const, result })),
364+
sleep(timeoutMs).then(() => ({ kind: 'timeout' as const })),
365+
])
366+
} catch (childError) {
367+
// child.result() rejects when the child workflow throws (shipsecWorkflowRun
368+
// always throws on failure rather than returning { success: false }).
369+
// Record NODE_FAILED so the UI shows the node as failed instead of stuck running.
370+
const message = childError instanceof Error ? childError.message : String(childError)
371+
await recordTraceEventActivity({
372+
type: 'NODE_FAILED',
373+
runId: input.runId,
374+
nodeRef: action.ref,
375+
timestamp: new Date().toISOString(),
376+
message,
377+
level: 'error',
378+
error: {
379+
message,
380+
type: 'SubWorkflowError',
381+
details: { childRunId },
382+
},
383+
context: {
384+
activityId: 'workflow-orchestration',
385+
childRunId,
386+
},
387+
})
388+
throw childError
389+
}
363390

364391
if (outcome.kind === 'timeout') {
365-
child.cancel()
392+
const externalHandle = getExternalWorkflowHandle(child.workflowId)
393+
await externalHandle.cancel()
366394

367395
await recordTraceEventActivity({
368396
type: 'NODE_FAILED',

0 commit comments

Comments
 (0)