Skip to content

Commit 6fa40c7

Browse files
committed
feat(workflows): selected outputs on sync runs + cross-run logs query augmentation
selectedOutputs was artificially stream-only: a sync run rejected it with a 400 and pointed at a second call (runs get) that resolves block ids only. The sync path already holds the full execution result, so selection now answers in the same response: blockOutputs keyed by the request's selector strings, names resolved against the exact state being run, absent paths omitted, failed and timed-out runs including the blocks that did run, values compacted like output. Route guard deleted, resume sends null, contract + openapi + cli-api + cli-docs regenerated, CLI --select-output works on a plain sync run (only --async still rejects it, locally, with the dialect hint). New agent-cli augmentation: logs query <workflowId> --block <name> [--field <path>] [--where <path>=<value>] [--status] [--trigger] [--limit] — one row per run across run history, recursive span walk, last match per run wins. The augmentation layer gains command-local flag parsing (--flag value / --flag=value / bare) to support it; positional-only commands are unchanged. Autopsy fix: the formatter neutral-path workaround from the display-frames commit dropped the caller's extension, so generate:openapi (JSON) aborted with a biome parse error while .ts generators stayed green. The extension now survives; regression test added and wired into test:generators. Claude-Session: https://claude.ai/code/session_01CgaxNAaeD3taGdghbXn17w
1 parent 17a02bf commit 6fa40c7

23 files changed

Lines changed: 669 additions & 105 deletions

File tree

apps/docs/content/docs/cli/reference.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5157,7 +5157,7 @@ sim workflows run <workflowId> [options]
51575157
| `--input <json\|@file>` | No | Trigger input as JSON (JSON, or @path / @- to read a file or stdin). |
51585158
| `--async` | No | Queue the run and return immediately. |
51595159
| `--execution-timeout-seconds <value>` | No | Requested server-side timeout for an asynchronous run, in seconds. An upper bound, not the effective timeout: the run uses the smaller of this value and the plan's execution timeout, so requesting more than the plan allows silently yields the plan timeout. Rejected with `400` unless `async` is true. |
5160-
| `--select-output <value...>` | No | Return streamed outputs as blockName.path or childWorkflowId.blockName.path; selecting a child workflow applies to every invocation, requires --follow (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
5160+
| `--select-output <value...>` | No | Return blockName.field values (e.g. agent_1.content) — in blockOutputs on a sync run, or from the streamed result with --follow; missing fields are omitted. Not available with --async (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
51615161
| `--include-file-base64` | No | Inline eligible output files as base64 content. Rejected when `async` is true. |
51625162
| `--no-include-file-base64` | No | Send --include-file-base64 as false. |
51635163
| `--base64-max-bytes <value>` | No | Maximum total bytes of file content to inline as base64, lowering but never raising the server limit of 16 MiB. Rejected when `async` is true. |

apps/docs/content/docs/cli/workflows.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,7 @@ sim workflows run <workflowId> [options]
532532
| `--input <json\|@file>` | No | Trigger input as JSON (JSON, or @path / @- to read a file or stdin). |
533533
| `--async` | No | Queue the run and return immediately. |
534534
| `--execution-timeout-seconds <value>` | No | Requested server-side timeout for an asynchronous run, in seconds. An upper bound, not the effective timeout: the run uses the smaller of this value and the plan's execution timeout, so requesting more than the plan allows silently yields the plan timeout. Rejected with `400` unless `async` is true. |
535-
| `--select-output <value...>` | No | Return streamed outputs as blockName.path or childWorkflowId.blockName.path; selecting a child workflow applies to every invocation, requires --follow (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
535+
| `--select-output <value...>` | No | Return blockName.field values (e.g. agent_1.content) — in blockOutputs on a sync run, or from the streamed result with --follow; missing fields are omitted. Not available with --async (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
536536
| `--include-file-base64` | No | Inline eligible output files as base64 content. Rejected when `async` is true. |
537537
| `--no-include-file-base64` | No | Send --include-file-base64 as false. |
538538
| `--base64-max-bytes <value>` | No | Maximum total bytes of file content to inline as base64, lowering but never raising the server limit of 16 MiB. Rejected when `async` is true. |

apps/docs/openapi-v2-workflows.json

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5169,7 +5169,14 @@
51695169
},
51705170
"kind": {
51715171
"type": "string",
5172-
"enum": ["credential", "resource", "custom-tool", "mcp-tool", "skill"],
5172+
"enum": [
5173+
"credential",
5174+
"resource",
5175+
"custom-tool",
5176+
"mcp-tool",
5177+
"skill",
5178+
"block-output"
5179+
],
51735180
"description": "What kind of entity the reference was expected to name."
51745181
},
51755182
"reason": {
@@ -8593,10 +8600,6 @@
85938600
"StoredChatDeploymentOutputConfig": {
85948601
"type": "object",
85958602
"properties": {
8596-
"workflowId": {
8597-
"description": "Child workflow containing the selected block. Omitted for the deployed workflow.",
8598-
"type": "string"
8599-
},
86008603
"blockId": {
86018604
"type": "string",
86028605
"description": "Block whose output the chat streams."
@@ -8906,11 +8909,6 @@
89068909
"ChatDeploymentOutputConfig": {
89078910
"type": "object",
89088911
"properties": {
8909-
"workflowId": {
8910-
"description": "Child workflow containing the selected block. Omit for the deployed workflow.",
8911-
"type": "string",
8912-
"minLength": 1
8913-
},
89148912
"blockId": {
89158913
"type": "string",
89168914
"minLength": 1,
@@ -9103,6 +9101,23 @@
91039101
"output": {
91049102
"description": "Workflow output, including partial output on failure."
91059103
},
9104+
"blockOutputs": {
9105+
"anyOf": [
9106+
{
9107+
"type": "object",
9108+
"propertyNames": {
9109+
"type": "string"
9110+
},
9111+
"additionalProperties": {
9112+
"description": "Output value produced by one workflow block."
9113+
}
9114+
},
9115+
{
9116+
"type": "null"
9117+
}
9118+
],
9119+
"description": "Outputs of the blocks named by `selectedOutputs`, keyed by those selector strings, or null when none were requested. Selectors whose block did not run or whose path is absent are omitted; failed runs include the outputs of the blocks that did run."
9120+
},
91069121
"error": {
91079122
"anyOf": [
91089123
{
@@ -9130,7 +9145,7 @@
91309145
"minimum": 0
91319146
}
91329147
},
9133-
"required": ["runId", "workflowId", "status", "output", "error"],
9148+
"required": ["runId", "workflowId", "status", "output", "blockOutputs", "error"],
91349149
"additionalProperties": false,
91359150
"title": "Workflow run result",
91369151
"description": "Synchronous workflow run output and in-band execution status. Run failures are reported in band, not as HTTP errors — a run that exceeds its execution timeout returns HTTP 200 with `status: \"failed\"` and `error.code: \"TIMEOUT\"`, so branch on `status`."
@@ -9156,6 +9171,7 @@
91569171
"output": {
91579172
"result": "Ticket routed to Support"
91589173
},
9174+
"blockOutputs": null,
91599175
"error": null,
91609176
"startedAt": "2026-08-09T18:04:10.000Z",
91619177
"endedAt": "2026-08-09T18:04:11.000Z",
@@ -9314,7 +9330,7 @@
93149330
"type": "boolean"
93159331
},
93169332
"selectedOutputs": {
9317-
"description": "Block output references to include in a streamed response. Use `<blockName>.<outputPath>` for the executed workflow or `<childWorkflowId>.<blockName>.<outputPath>` for a child workflow; block names are normalized workflow reference names. Selecting a child workflow applies to every invocation of it. Requires `stream: true` — it shapes the streamed envelope only, so it is rejected on a sync request and when `async` is true. To narrow a finished run, pass `selectedOutputs` to the run resource instead.",
9333+
"description": "Block output references to include in the response, as `blockId`, `blockId.path`, or `BlockName.path` (resolved against the workflow state being run). On a sync request the named outputs come back in `blockOutputs`, keyed by these selector strings; on a stream they shape the streamed envelope. Selectors that resolve to no block or no value are omitted. Rejected when `async` is true — a queued run has produced nothing to select; narrow the finished run via the run resource instead.",
93189334
"maxItems": 100,
93199335
"type": "array",
93209336
"items": {
@@ -9892,6 +9908,7 @@
98929908
"output": {
98939909
"result": "Ticket routed to Support"
98949910
},
9911+
"blockOutputs": null,
98959912
"error": null,
98969913
"startedAt": "2026-08-09T18:04:10.000Z",
98979914
"endedAt": "2026-08-09T18:04:11.000Z",
@@ -10006,7 +10023,7 @@
1000610023
"description": "Whether a paused execution was cancelled."
1000710024
},
1000810025
"reason": {
10009-
"description": "Machine-readable cancellation outcome, present on every cancellation including full successes. `recorded` and `queue_cancelled` are successful cancellation values. `already_cancelled`, `already_completed`, and `already_failed` mean the run had already reached that terminal state, so nothing was cancelled and `durablyRecorded` is false. The remaining values identify a degraded or incomplete cancellation step.",
10026+
"description": "Machine-readable cancellation outcome, present on every cancellation including full successes. `recorded` is the success value. `already_cancelled`, `already_completed`, and `already_failed` mean the run had already reached that terminal state, so nothing was cancelled and `durablyRecorded` is false. `redis_unavailable` and `redis_write_failed` mean the distributed cancellation signal was not written, so an already-running execution may not observe the cancellation. `paused_event_publish_failed` and `paused_database_cancel_failed` name the failing step for a paused run.",
1001010027
"type": "string",
1001110028
"enum": [
1001210029
"recorded",
@@ -10016,10 +10033,7 @@
1001610033
"redis_unavailable",
1001710034
"redis_write_failed",
1001810035
"paused_event_publish_failed",
10019-
"paused_database_cancel_failed",
10020-
"queue_cancelled",
10021-
"active_resume_signal_failed",
10022-
"cancellation_not_finalized"
10036+
"paused_database_cancel_failed"
1002310037
]
1002410038
}
1002510039
},

apps/sim/app/api/v2/workflows/[workflowId]/execute/route.test.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ describe('POST /api/v2/workflows/[workflowId]/execute', () => {
340340
workflowId: 'workflow-1',
341341
status: 'completed',
342342
output: { result: 'done' },
343+
blockOutputs: null,
343344
error: null,
344345
durationMs: 42,
345346
})
@@ -631,12 +632,47 @@ describe('POST /api/v2/workflows/[workflowId]/execute', () => {
631632
expect(mockPreprocessExecution).not.toHaveBeenCalled()
632633
})
633634

634-
it('rejects selectedOutputs on a sync request rather than ignoring it', async () => {
635-
const res = await callExecute({ selectedOutputs: ['agent_1.content'] })
635+
it('returns blockOutputs for selectedOutputs on a sync request', async () => {
636+
const agentBlockId = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'
637+
mockLoadDeployedWorkflowState.mockResolvedValue({
638+
blocks: { [agentBlockId]: { id: agentBlockId, name: 'Agent 1' } },
639+
edges: [],
640+
loops: {},
641+
parallels: {},
642+
variables: {},
643+
})
644+
mockExecuteWorkflowCore.mockResolvedValue({
645+
success: true,
646+
output: { result: 'done' },
647+
logs: [
648+
{
649+
blockId: agentBlockId,
650+
blockName: 'Agent 1',
651+
startedAt: 's',
652+
endedAt: 'e',
653+
durationMs: 5,
654+
success: true,
655+
output: { content: 'hi', tokens: { total: 7 } },
656+
},
657+
],
658+
metadata: {
659+
duration: 42,
660+
startTime: '2026-07-31T00:00:00.000Z',
661+
endTime: '2026-07-31T00:00:01.000Z',
662+
},
663+
})
636664

637-
expect(res.status).toBe(400)
638-
expect((await res.json()).error.message).toContain('selectedOutputs requires stream: true')
639-
expect(mockPreprocessExecution).not.toHaveBeenCalled()
665+
const res = await callExecute({
666+
input: {},
667+
selectedOutputs: ['Agent 1.content', 'Agent 1.absent', agentBlockId],
668+
})
669+
670+
expect(res.status).toBe(200)
671+
const body = await res.json()
672+
expect(body.data.blockOutputs).toEqual({
673+
'Agent 1.content': 'hi',
674+
[agentBlockId]: { content: 'hi', tokens: { total: 7 } },
675+
})
640676
})
641677

642678
it.each(['includeThinking', 'includeToolCalls'])(

apps/sim/app/api/v2/workflows/[workflowId]/execute/route.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -268,19 +268,6 @@ export const POST = withRouteHandler(
268268
'Async execution does not support streaming or output-shaping options'
269269
)
270270
}
271-
/**
272-
* `selectedOutputs` shapes the streamed envelope only — the sync path
273-
* returns the workflow's own final output and never reads it. Accepting
274-
* it silently answered a full, unselected body to a caller who believed
275-
* they had narrowed it, so the option is refused where it does nothing
276-
* and the two paths that honour selection are named instead.
277-
*/
278-
if (body.selectedOutputs?.length && !body.stream) {
279-
return v2Error(
280-
'BAD_REQUEST',
281-
'selectedOutputs requires stream: true. For a completed run, request the run resource with ?selectedOutputs= instead.'
282-
)
283-
}
284271
const hasAgentStreamOptions = hasAgentStreamPolicy({
285272
includeThinking: body.includeThinking,
286273
includeToolCalls: body.includeToolCalls,
@@ -436,6 +423,7 @@ export const POST = withRouteHandler(
436423
workflowId: result.workflowId,
437424
status: result.status,
438425
output: result.output ?? null,
426+
blockOutputs: result.blockOutputs ?? null,
439427
error: result.error,
440428
startedAt: result.startedAt,
441429
endedAt: result.endedAt,

apps/sim/app/api/v2/workflows/[workflowId]/runs/[runId]/resume/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ export const POST = withRouteHandler(
111111
workflowId,
112112
status: result.status as 'completed' | 'failed' | 'paused' | 'cancelled',
113113
output: result.output ?? null,
114+
// Resume has no request body to name selectors in, so selection never applies here.
115+
blockOutputs: null,
114116
error:
115117
typeof result.error === 'string'
116118
? classifyExecutionError(new Error(result.error))

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ const RUN_RESULT_EXAMPLE = {
179179
workflowId: WORKFLOW_ID,
180180
status: 'completed',
181181
output: { result: 'Ticket routed to Support' },
182+
blockOutputs: null,
182183
error: null,
183184
startedAt: '2026-08-09T18:04:10.000Z',
184185
endedAt: '2026-08-09T18:04:11.000Z',

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1293,7 +1293,7 @@ export const v2ExecuteWorkflowBodySchema = z
12931293
.max(100)
12941294
.optional()
12951295
.describe(
1296-
'Block output references to include in a streamed response. Use `<blockName>.<outputPath>` for the executed workflow or `<childWorkflowId>.<blockName>.<outputPath>` for a child workflow; block names are normalized workflow reference names. Selecting a child workflow applies to every invocation of it. Requires `stream: true` — it shapes the streamed envelope only, so it is rejected on a sync request and when `async` is true. To narrow a finished run, pass `selectedOutputs` to the run resource instead.'
1296+
'Block output references to include in the response. Use `<blockName>.<outputPath>` for the executed workflow or `<childWorkflowId>.<blockName>.<outputPath>` for a child workflow; block names are normalized workflow reference names, and selecting a child workflow applies to every invocation of it. On a sync request the named outputs come back in `blockOutputs`, keyed by these selector strings; on a stream they shape the streamed envelope. Selectors that resolve to no block or no value are omitted. Rejected when `async` is true — a queued run has produced nothing to select; narrow the finished run via the run resource instead.'
12971297
),
12981298
includeThinking: z
12991299
.boolean()
@@ -1365,6 +1365,12 @@ export const v2ExecuteWorkflowDataSchema = z
13651365
.enum(['completed', 'failed', 'paused', 'cancelled'])
13661366
.describe('Terminal or paused run status.'),
13671367
output: z.unknown().describe('Workflow output, including partial output on failure.'),
1368+
blockOutputs: z
1369+
.record(z.string(), z.unknown().describe('Output value produced by one workflow block.'))
1370+
.nullable()
1371+
.describe(
1372+
'Outputs of the blocks named by `selectedOutputs`, keyed by those selector strings, or null when none were requested. Selectors whose block did not run or whose path is absent are omitted; failed runs include the outputs of the blocks that did run.'
1373+
),
13681374
error: v2ExecutionErrorSchema
13691375
.nullable()
13701376
.describe('Structured execution failure, or null when none occurred.'),

apps/sim/lib/mothership/tools/cli-tool-display.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ export const CLI_TOOL_TITLES: Record<string, string> = {
224224
cli_workspaces_members: 'Listing workspace members',
225225
// Agent-only CLI augmentations
226226
cli_files_grep: 'Searching file contents',
227+
cli_logs_query: 'Querying run history',
227228
cli_workflow_blocks: 'Listing workflow blocks',
228229
cli_workflow_deps: 'Tracing block inputs',
229230
cli_workflow_edges: 'Reading workflow wiring',

0 commit comments

Comments
 (0)