Skip to content

Commit 515a95f

Browse files
feat(workflows): support ?selectedOutputs= on execution status endpoint
Returns per-block outputs filtered by selectedOutputs paths (same shape as the execute endpoint). Reads from executionData.traceSpans, walks children recursively, and resolves dot-paths into each block's output. Bare blockId returns the full output.
1 parent 08e6b2b commit 515a95f

2 files changed

Lines changed: 69 additions & 3 deletions

File tree

apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,56 @@ const logger = createLogger('WorkflowExecutionStatusAPI')
1616

1717
type LogStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
1818

19+
interface TraceSpanShape {
20+
blockId?: string
21+
output?: Record<string, unknown>
22+
children?: TraceSpanShape[]
23+
}
24+
1925
interface ExecutionDataShape {
2026
finalOutput?: { error?: string } & Record<string, unknown>
2127
error?: { message?: string } | string
2228
completionFailure?: string
29+
traceSpans?: TraceSpanShape[]
30+
}
31+
32+
function collectBlockOutputs(spans: TraceSpanShape[] | undefined): Map<string, unknown> {
33+
const map = new Map<string, unknown>()
34+
const visit = (list?: TraceSpanShape[]): void => {
35+
if (!list) return
36+
for (const span of list) {
37+
if (span.blockId && span.output !== undefined && !map.has(span.blockId)) {
38+
map.set(span.blockId, span.output)
39+
}
40+
if (span.children) visit(span.children)
41+
}
42+
}
43+
visit(spans)
44+
return map
45+
}
46+
47+
function resolvePath(value: unknown, path: string[]): unknown {
48+
let current: unknown = value
49+
for (const segment of path) {
50+
if (current == null || typeof current !== 'object') return undefined
51+
current = (current as Record<string, unknown>)[segment]
52+
}
53+
return current
54+
}
55+
56+
function pickSelectedOutputs(
57+
selectedOutputs: string[],
58+
blockOutputs: Map<string, unknown>
59+
): Record<string, unknown> {
60+
const out: Record<string, unknown> = {}
61+
for (const selector of selectedOutputs) {
62+
const [head, ...rest] = selector.split('.')
63+
if (!head) continue
64+
if (!blockOutputs.has(head)) continue
65+
const blockValue = blockOutputs.get(head)
66+
out[selector] = rest.length === 0 ? blockValue : resolvePath(blockValue, rest)
67+
}
68+
return out
2369
}
2470

2571
function pickEarliestPausePoint(points: PausePoint[]): PausePoint | null {
@@ -60,7 +106,7 @@ export const GET = withRouteHandler(
60106
const parsed = await parseRequest(getWorkflowExecutionContract, request, context)
61107
if (!parsed.success) return parsed.response
62108
const { id: workflowId, executionId } = parsed.data.params
63-
const { includeOutput } = parsed.data.query
109+
const { includeOutput, selectedOutputs } = parsed.data.query
64110

65111
const access = await validateWorkflowAccess(request, workflowId, false)
66112
if (access.error) {
@@ -137,9 +183,16 @@ export const GET = withRouteHandler(
137183

138184
const error = status === 'failed' ? extractError(logRow.executionData) : null
139185

186+
const executionData = logRow.executionData as ExecutionDataShape | undefined
187+
140188
const finalOutput =
141-
includeOutput && status === 'completed' && logRow.executionData
142-
? ((logRow.executionData as ExecutionDataShape).finalOutput ?? null)
189+
includeOutput && status === 'completed' && executionData
190+
? (executionData.finalOutput ?? null)
191+
: null
192+
193+
const blockOutputs =
194+
selectedOutputs.length > 0
195+
? pickSelectedOutputs(selectedOutputs, collectBlockOutputs(executionData?.traceSpans))
143196
: null
144197

145198
const response: WorkflowExecutionStatusResponse = {
@@ -155,6 +208,7 @@ export const GET = withRouteHandler(
155208
cost,
156209
error,
157210
finalOutput,
211+
blockOutputs,
158212
}
159213

160214
logger.debug('Fetched execution status', {

apps/sim/lib/api/contracts/workflows.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ const workflowExecutionStatusResponseSchema = z.object({
541541
cost: z.object({ total: z.number() }).nullable(),
542542
error: z.string().nullable(),
543543
finalOutput: z.unknown().nullable(),
544+
blockOutputs: z.record(z.string(), z.unknown()).nullable(),
544545
})
545546

546547
export type WorkflowExecutionStatusResponse = z.output<typeof workflowExecutionStatusResponseSchema>
@@ -550,6 +551,17 @@ const workflowExecutionStatusQuerySchema = z.object({
550551
.enum(['true', 'false'])
551552
.optional()
552553
.transform((value) => value === 'true'),
554+
selectedOutputs: z
555+
.string()
556+
.optional()
557+
.transform((value) =>
558+
value
559+
? value
560+
.split(',')
561+
.map((s) => s.trim())
562+
.filter(Boolean)
563+
: []
564+
),
553565
})
554566

555567
const cancelWorkflowExecutionResponseSchema = z.object({

0 commit comments

Comments
 (0)