Skip to content

Commit 24aab3f

Browse files
authored
feat(api): run a pinned deployment version and expose paused run points in v2 (#9854)
1 parent 02293a9 commit 24aab3f

39 files changed

Lines changed: 2752 additions & 113 deletions

‎apps/docs/content/docs/workflows/deployment/api.mdx‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ You need workspace **Admin** access and the applicable deployment permission. Te
1414

1515
Ordinary API calls use the active snapshot. Editing the draft does not change that version. Deploy an update before expecting a normal API call to use your changes.
1616

17+
### Run an earlier version [#run-a-version]
18+
19+
To run a specific deployment version instead of the active one, name its number in `run.version`. The workflow must still be deployed, and the caller needs workspace **Write** access with an API key or OAuth access token; anonymous public-API callers cannot pin a version. An unknown version returns `404`. The run's status reports the version it ran in `version`.
20+
21+
```bash
22+
curl -X POST 'https://www.sim.ai/api/v2/workflows/{workflow-id}/execute' \
23+
-H "X-API-Key: $SIM_API_KEY" \
24+
-H 'Content-Type: application/json' \
25+
-d '{"input":{"message":"Summarize this article"},"run":{"source":"deployment","version":3}}'
26+
```
27+
1728
## Authentication
1829

1930
Send a valid Sim API key in `X-API-Key`, or use a supported OAuth access token. Deployed workflow execution requires access to the workflow's workspace; OAuth callers also need the operation's `api:write` scope. See [API authentication](/api-reference/authentication).
@@ -249,6 +260,8 @@ A completed response includes the result when requested:
249260

250261
Stop polling on `completed`, `failed`, or `cancelled`. A paused Human run requires submitted input; a time pause can resume automatically. See [Human](/workflows/blocks/human-in-the-loop) and [Wait](/workflows/blocks/wait). Resuming an originally streaming run through the v2 resume endpoint returns an async receipt, not an SSE continuation.
251262

263+
A paused run lists every point it waits at in `paused.points`: the block, whether it is a `human` or `time` pause, its resume state, any parallel branch or loop iteration, its place in the resume queue, and the `inputFields` a Human pause asks for. With `includeOutput=true`, each point also carries the reviewer `display` content with resolved secrets redacted; organization permission groups that hide execution data withhold it, along with `inputFields`, whose labels and descriptions are resolved from run data. Resume a point by sending its `contextId` to `POST /api/v2/workflows/{workflow-id}/runs/{runId}/resume`.
264+
252265
## Select outputs and handle large values [#oversized-outputs]
253266

254267
`selectedOutputs` can project smaller fields. For a synchronous v2 call, selected values appear in `data.blockOutputs`, separately from `data.output`.

‎apps/docs/openapi-v2-workflows.json‎

Lines changed: 252 additions & 6 deletions
Large diffs are not rendered by default.

‎apps/docs/scripts/fixtures/content-baseline.json‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181727,6 +181727,13 @@
181727181727
],
181728181728
"expressions": [],
181729181729
"codeFences": [
181730+
{
181731+
"lang": "bash",
181732+
"meta": null,
181733+
"sha256": "32c20ca5dbc0941290361d3ddba31d3585a80493e65c486960e2660fae4c03d0",
181734+
"bytes": 243,
181735+
"line": 21
181736+
},
181730181737
{
181731181738
"lang": "bash",
181732181739
"meta": null,

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ import {
4040
executeManualWorkflowFromBlockOperation,
4141
executeManualWorkflowOperation,
4242
} from '@/lib/workflows/application/execute-manual-workflow'
43-
import { executeWorkflowOperation } from '@/lib/workflows/application/execute-workflow'
43+
import {
44+
executeWorkflowOperation,
45+
executeWorkflowVersionOperation,
46+
} from '@/lib/workflows/application/execute-workflow'
4447
import { workflowOperations } from '@/lib/workflows/application/operations'
4548
import {
4649
type ExecuteWorkflowServiceFailure,
@@ -316,6 +319,8 @@ type V2ExecuteWorkflowRouteContext = {
316319
*
317320
* - Auth: `X-API-Key` (personal/workspace) or the anonymous public-API path for
318321
* workflows deployed with `isPublicApi` (actor = owner; sync/stream only).
322+
* - `run: { source: 'deployment', version }` runs that deployment version under
323+
* the write-role `workflows.execute_version` operation; keyed callers only.
319324
* - `async: true` (body flag — v2 has no mode headers) → 202
320325
* `{ data: { runId, statusUrl } }`; poll the v2 runs resource.
321326
* - `stream: true` → SSE passthrough (no `{data}` envelope on event frames).
@@ -422,6 +427,7 @@ export const POST = withRouteHandler(
422427
if (!parsed.success) return parsed.response
423428
const body = parsed.data.body
424429
const manualRun = body.run?.source === 'manual' ? body.run : undefined
430+
const pinnedVersion = body.run?.source === 'deployment' ? body.run.version : undefined
425431

426432
if (manualRun && !apiKeyPrincipal) {
427433
return v2Error(
@@ -439,6 +445,9 @@ export const POST = withRouteHandler(
439445
) {
440446
return v2Error('BAD_REQUEST', 'input and run.entry.useMockPayload cannot be combined')
441447
}
448+
if (pinnedVersion !== undefined && isPublicApiAccess) {
449+
return v2Error('BAD_REQUEST', 'run.version requires an OAuth access token or API key')
450+
}
442451

443452
const detachable = body.waitTimeoutSeconds !== undefined
444453
if (detachable && (body.async || body.stream)) {
@@ -544,6 +553,17 @@ export const POST = withRouteHandler(
544553
},
545554
request: req,
546555
})
556+
} else if (pinnedVersion !== undefined) {
557+
result = await executeWorkflowVersionOperation.execute({
558+
principal: apiKeyPrincipal,
559+
input: {
560+
...commonInput,
561+
input: body.input ?? {},
562+
mode: body.async ? 'async' : body.stream ? 'stream' : syncMode,
563+
version: pinnedVersion,
564+
},
565+
request: req,
566+
})
547567
} else {
548568
result = await executeWorkflowOperation.execute({
549569
principal: apiKeyPrincipal,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const baseStatus = {
5959
startedAt: '2026-07-31T00:00:00.000Z',
6060
endedAt: '2026-07-31T00:00:05.000Z',
6161
totalDurationMs: 5000,
62+
deploymentVersion: null,
6263
paused: null,
6364
cost: { total: 0.02 },
6465
error: 'Send Email: Invalid credentials',

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,13 @@ export const GET = defineV2JsonRoute({
4545
startedAt: status.startedAt,
4646
endedAt: status.endedAt,
4747
durationMs: status.totalDurationMs,
48-
paused: status.paused ? v2WorkflowRunStatusSchema.shape.paused.parse(status.paused) : null,
48+
version: status.deploymentVersion ?? null,
49+
paused: status.paused
50+
? v2WorkflowRunStatusSchema.shape.paused.parse({
51+
...status.paused,
52+
points: status.pausePoints ?? [],
53+
})
54+
: null,
4955
cost: status.cost,
5056
error: status.error ? classifyExecutionError(new Error(status.error)) : null,
5157
output: status.finalOutput,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ describe('internal execution status route projection subject', () => {
5353
startedAt: '2026-08-05T12:00:00.000Z',
5454
endedAt: null,
5555
totalDurationMs: null,
56+
deploymentVersion: null,
5657
paused: null,
5758
cost: null,
5859
error: null,

0 commit comments

Comments
 (0)