From 6931ea6ab674be0d0d822d7048d77bb462918adf Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 12 Aug 2026 00:59:53 -0700 Subject: [PATCH 1/4] fix(v2): derive the log and run status enums from the persisted status list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `GET /api/v2/logs` and `GET /api/v2/logs/{runId}` parse the raw `workflow_execution_logs.status` column against a six-value enum that omits `paused`, so a run holding that value returns 500. The list response is validated whole-page, so one such row 500s every page it lands on, and the row is durable until the run is resumed, cancelled, or failed. `paused` is not written by an ordinary human-in-the-loop pause — that path persists `pending` (logging-session.ts:1180). It is written by `PauseResumeManager.markResumeAttemptFailed`, which fires on any `ResumeAdmissionError`: a workspace over its usage limit, an archived or undeployed workflow, or a concurrent resume losing the claim race. That is a routine business path. The enum was supposed to be protected by an `AssertNever` exhaustiveness gate, but the gate was vacuous: it compared against `PersistedWorkflowExecutionStatus`, a hand-written union that was itself missing `paused`, because the write goes through a raw `sql` CASE fragment Drizzle cannot type-check. Adding `paused` to both lists would leave the same vacuous gate in place for the next status. Instead, `PERSISTED_WORKFLOW_EXECUTION_STATUSES` becomes the single runtime source of truth, `PersistedWorkflowExecutionStatus` is derived from it, and both v2 contracts derive their enums from the const rather than re-declaring them. Both surfaces pass the column through verbatim, so their reported set is the persisted set by definition — there is no editorial choice for a gate to force, only the question of whether a newly persisted status should be public, which the option-list tests now pin. The `[...V2_PERSISTED_RUN_STATUSES, 'paused']` append on the runs contract is deleted rather than adjusted; it would otherwise be a duplicate. Alternatives rejected: - A `.catch()` or `safeParse` in the presenters is dead code: `v2-json-route.ts:271` re-parses the whole body with the same schema. - Normalizing `markResumeAttemptFailed` to write `pending` would remove the distinction the resume claim query at human-in-the-loop-manager.ts:973 relies on, and leaves the contract wrong for any other future status. - Typing the Drizzle column does not help: the offending write is a raw `sql` fragment, and `packages/db` cannot import the app's status list. The v2 workflows spec changes are reordering and description only — the value set there already contained `paused`. The v2 logs spec gains `paused`, which is additive and safe while the whole `/api/v2` surface is behind the off-by-default `v2-api` flag; it must land before v2 GA, after which it would be breaking. --- apps/docs/openapi-v2-logs.json | 24 ++++++-- apps/docs/openapi-v2-workflows.json | 10 ++-- .../sim/app/api/v2/logs/[runId]/route.test.ts | 15 +++++ apps/sim/app/api/v2/logs/route.test.ts | 18 ++++++ .../lib/api/contracts/v2/log-status.test.ts | 26 +++++++++ apps/sim/lib/api/contracts/v2/logs.ts | 31 +++-------- .../contracts/v2/workflow-run-status.test.ts | 55 ++++++++++++------- apps/sim/lib/api/contracts/v2/workflows.ts | 43 ++++----------- apps/sim/lib/logs/types.ts | 30 ++++++++-- packages/db/schema.ts | 2 +- 10 files changed, 163 insertions(+), 91 deletions(-) create mode 100644 apps/sim/lib/api/contracts/v2/log-status.test.ts diff --git a/apps/docs/openapi-v2-logs.json b/apps/docs/openapi-v2-logs.json index 6ee095fb60b..d4df4c2b0df 100644 --- a/apps/docs/openapi-v2-logs.json +++ b/apps/docs/openapi-v2-logs.json @@ -640,8 +640,16 @@ }, "status": { "type": "string", - "enum": ["pending", "running", "redacting", "completed", "failed", "cancelled"], - "description": "Current execution status. `redacting` is transient while run output is scrubbed." + "enum": [ + "pending", + "running", + "paused", + "redacting", + "completed", + "failed", + "cancelled" + ], + "description": "Current execution status. `redacting` is transient while run output is scrubbed. `paused` is reported when a resume attempt did not run to completion and the run is waiting to be resumed again." }, "level": { "type": "string", @@ -1028,8 +1036,16 @@ }, "status": { "type": "string", - "enum": ["pending", "running", "redacting", "completed", "failed", "cancelled"], - "description": "Current execution status. `redacting` is transient while run output is scrubbed." + "enum": [ + "pending", + "running", + "paused", + "redacting", + "completed", + "failed", + "cancelled" + ], + "description": "Current execution status. `redacting` is transient while run output is scrubbed. `paused` is reported when a resume attempt did not run to completion and the run is waiting to be resumed again." }, "level": { "type": "string", diff --git a/apps/docs/openapi-v2-workflows.json b/apps/docs/openapi-v2-workflows.json index 3d650764b1a..f8aadfc6d0e 100644 --- a/apps/docs/openapi-v2-workflows.json +++ b/apps/docs/openapi-v2-workflows.json @@ -3923,13 +3923,13 @@ "enum": [ "pending", "running", + "paused", "redacting", "completed", "failed", - "cancelled", - "paused" + "cancelled" ], - "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed." + "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` is reported while the run is waiting at a human-in-the-loop pause point." }, "trigger": { "type": "string", @@ -4063,14 +4063,14 @@ "enum": [ "pending", "running", + "paused", "redacting", "completed", "failed", "cancelled", - "paused", "queued" ], - "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed." + "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` is reported while the run is waiting at a human-in-the-loop pause point." }, "trigger": { "anyOf": [ diff --git a/apps/sim/app/api/v2/logs/[runId]/route.test.ts b/apps/sim/app/api/v2/logs/[runId]/route.test.ts index 8b0c445a4d5..27d0226bda7 100644 --- a/apps/sim/app/api/v2/logs/[runId]/route.test.ts +++ b/apps/sim/app/api/v2/logs/[runId]/route.test.ts @@ -94,6 +94,21 @@ describe('GET /api/v2/logs/[runId]', () => { }) }) + it('serves a run whose persisted status is paused', async () => { + mocks.execute.mockResolvedValue({ + log: { ...log, status: 'paused' }, + workflowFolderPath: '/agents', + executionData: { traceSpans: [], finalOutput: null }, + }) + + const response = await GET(new NextRequest('http://localhost:3000/api/v2/logs/run-1'), { + params: Promise.resolve({ runId: 'run-1' }), + }) + + expect(response.status).toBe(200) + expect((await response.json()).data).toMatchObject({ runId: 'run-1', status: 'paused' }) + }) + it('conceals canonical workspace authorization as log not-found', async () => { mocks.execute.mockRejectedValueOnce(new NoWorkspaceAccessError()) diff --git a/apps/sim/app/api/v2/logs/route.test.ts b/apps/sim/app/api/v2/logs/route.test.ts index 069bb3ed605..74b5755c412 100644 --- a/apps/sim/app/api/v2/logs/route.test.ts +++ b/apps/sim/app/api/v2/logs/route.test.ts @@ -98,6 +98,24 @@ describe('GET /api/v2/logs', () => { }) }) + it('serves a run whose persisted status is paused', async () => { + mocks.execute.mockResolvedValue({ + items: [{ log: { ...log, status: 'paused' }, executionData: null }], + nextCursor: null, + includeFullDetails: false, + includeFinalOutput: false, + includeTraceSpans: false, + }) + + const response = await GET( + new NextRequest(`http://localhost:3000/api/v2/logs?workspaceId=${WORKSPACE_ID}`) + ) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body.data[0]).toMatchObject({ runId: 'run-1', status: 'paused' }) + }) + it('rejects malformed cursors after admission and before protected reads', async () => { const response = await GET( new NextRequest( diff --git a/apps/sim/lib/api/contracts/v2/log-status.test.ts b/apps/sim/lib/api/contracts/v2/log-status.test.ts new file mode 100644 index 00000000000..8d48aac3c95 --- /dev/null +++ b/apps/sim/lib/api/contracts/v2/log-status.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest' +import { v2LogStatusSchema } from '@/lib/api/contracts/v2/logs' +import { PERSISTED_WORKFLOW_EXECUTION_STATUSES } from '@/lib/logs/types' + +/** + * Both log endpoints pass `workflow_execution_logs.status` through verbatim — unlike the + * run endpoints there is no `paused` overlay and no `queued` — so any drift between the + * reported enum and the persisted list 500s a whole page of results. + */ +describe('v2 log status schema', () => { + it('publishes exactly the persisted statuses', () => { + expect(v2LogStatusSchema.options).toEqual([ + 'pending', + 'running', + 'paused', + 'redacting', + 'completed', + 'failed', + 'cancelled', + ]) + }) + + it('stays derived from the persisted status list', () => { + expect(v2LogStatusSchema.options).toEqual([...PERSISTED_WORKFLOW_EXECUTION_STATUSES]) + }) +}) diff --git a/apps/sim/lib/api/contracts/v2/logs.ts b/apps/sim/lib/api/contracts/v2/logs.ts index 7e01f7c60be..18895a327cf 100644 --- a/apps/sim/lib/api/contracts/v2/logs.ts +++ b/apps/sim/lib/api/contracts/v2/logs.ts @@ -10,7 +10,7 @@ import { v2FolderPathSchema, v2TimestampSchema, } from '@/lib/api/contracts/v2/shared' -import type { PersistedWorkflowExecutionStatus } from '@/lib/logs/types' +import { PERSISTED_WORKFLOW_EXECUTION_STATUSES } from '@/lib/logs/types' /** * v2 logs contracts. The query schemas are reused verbatim from v1 (the request @@ -23,29 +23,16 @@ const v2LogCostSchema = z .nullable() .describe('Cost charged for the run, or null when unavailable.') /** - * Every status the execution logger can persist, including the transient - * `redacting` state written while a finished run's output is scrubbed. The - * column is free text, so a value missing here fails the response parse and - * turns a single row into a 500 for the whole page. `_ExhaustiveLogStatus` - * makes a future addition to the persisted union a compile error instead. + * Both log endpoints pass `workflow_execution_logs.status` through verbatim, so the + * reported set is exactly the persisted set — a value missing here fails the response + * parse, and because list validation is whole-page one such row turns an entire page + * into a 500. */ -const V2_LOG_STATUSES = [ - 'pending', - 'running', - 'redacting', - 'completed', - 'failed', - 'cancelled', -] as const satisfies readonly PersistedWorkflowExecutionStatus[] - -type AssertNever = T -type _ExhaustiveLogStatus = AssertNever< - Exclude -> - export const v2LogStatusSchema = z - .enum(V2_LOG_STATUSES) - .describe('Current execution status. `redacting` is transient while run output is scrubbed.') + .enum(PERSISTED_WORKFLOW_EXECUTION_STATUSES) + .describe( + 'Current execution status. `redacting` is transient while run output is scrubbed. `paused` is reported when a resume attempt did not run to completion and the run is waiting to be resumed again.' + ) /** Execution `files` is a per-run jsonb array of attachment metadata. */ const v2LogFilesSchema = z diff --git a/apps/sim/lib/api/contracts/v2/workflow-run-status.test.ts b/apps/sim/lib/api/contracts/v2/workflow-run-status.test.ts index 55801f4f3e1..5b656655bcb 100644 --- a/apps/sim/lib/api/contracts/v2/workflow-run-status.test.ts +++ b/apps/sim/lib/api/contracts/v2/workflow-run-status.test.ts @@ -4,36 +4,49 @@ import { v2WorkflowRunStatusFilterSchema, v2WorkflowRunStatusValueSchema, } from '@/lib/api/contracts/v2/workflows' -import type { PersistedWorkflowExecutionStatus } from '@/lib/logs/types' +import { PERSISTED_WORKFLOW_EXECUTION_STATUSES } from '@/lib/logs/types' /** - * The runtime mirror of the persisted union. `satisfies` keeps it honest against - * `PersistedWorkflowExecutionStatus`, and the `AssertNever` gate in the contract keeps - * that union honest against the reported enums, so a status added to the execution logger - * fails compilation in both places before it can 500 a response parse. + * Both run endpoints report `workflow_execution_logs.status`, overlaid with `paused` from + * `paused_executions`, so every reported value lands in the persisted set. These tests + * guard the two ways that can break: the derivation being replaced by a hand-maintained + * list again, and a status being added to the persisted set without anyone confirming it + * belongs on the public wire (and regenerating the OpenAPI specs). */ -const PERSISTED_STATUSES = [ - 'pending', - 'running', - 'redacting', - 'completed', - 'failed', - 'cancelled', -] as const satisfies readonly PersistedWorkflowExecutionStatus[] - describe('v2 workflow run status schemas', () => { - it.each(PERSISTED_STATUSES)('reports the persisted status %s on both run endpoints', (status) => { - expect(v2WorkflowRunListStatusValueSchema.parse(status)).toBe(status) - expect(v2WorkflowRunStatusValueSchema.parse(status)).toBe(status) + it('publishes exactly the persisted statuses on the run list', () => { + expect(v2WorkflowRunListStatusValueSchema.options).toEqual([ + 'pending', + 'running', + 'paused', + 'redacting', + 'completed', + 'failed', + 'cancelled', + ]) }) - it('reports the paused overlay on both run endpoints', () => { - expect(v2WorkflowRunListStatusValueSchema.parse('paused')).toBe('paused') - expect(v2WorkflowRunStatusValueSchema.parse('paused')).toBe('paused') + it('stays derived from the persisted status list', () => { + expect(v2WorkflowRunListStatusValueSchema.options).toEqual([ + ...PERSISTED_WORKFLOW_EXECUTION_STATUSES, + ]) + expect(v2WorkflowRunStatusValueSchema.options).toEqual([ + ...PERSISTED_WORKFLOW_EXECUTION_STATUSES, + 'queued', + ]) }) it('reports queued only where the job queue is consulted', () => { - expect(v2WorkflowRunStatusValueSchema.parse('queued')).toBe('queued') + expect(v2WorkflowRunStatusValueSchema.options).toEqual([ + 'pending', + 'running', + 'paused', + 'redacting', + 'completed', + 'failed', + 'cancelled', + 'queued', + ]) expect(v2WorkflowRunListStatusValueSchema.safeParse('queued').success).toBe(false) }) diff --git a/apps/sim/lib/api/contracts/v2/workflows.ts b/apps/sim/lib/api/contracts/v2/workflows.ts index 2359af203b2..b7f594419dc 100644 --- a/apps/sim/lib/api/contracts/v2/workflows.ts +++ b/apps/sim/lib/api/contracts/v2/workflows.ts @@ -36,7 +36,7 @@ import { workflowIdParamsSchema, } from '@/lib/api/contracts/workflows' import { MAX_WORKFLOW_EXECUTION_TIMEOUT_SECONDS } from '@/lib/billing/execution-timeout-defaults' -import type { PersistedWorkflowExecutionStatus } from '@/lib/logs/types' +import { PERSISTED_WORKFLOW_EXECUTION_STATUSES } from '@/lib/logs/types' export const V2_WORKFLOW_RUN_ID_HEADER = 'X-Run-Id' @@ -982,40 +982,19 @@ export const v2ResumeWorkflowContract = defineRouteContract({ }, }) -/** - * Every status the execution logger can persist into `workflow_execution_logs.status`, - * including the transient `redacting` state written while a finished run's output is - * scrubbed. The column is free text and both run endpoints pass it straight through, so - * a value missing here fails the response parse — and because list validation is - * whole-page, one such row turns an entire page into a 500. `_ExhaustiveRunStatus` makes - * a future addition to the persisted union a compile error instead. - */ -const V2_PERSISTED_RUN_STATUSES = [ - 'pending', - 'running', - 'redacting', - 'completed', - 'failed', - 'cancelled', -] as const satisfies readonly PersistedWorkflowExecutionStatus[] - -type AssertNever = T -type _ExhaustiveRunStatus = AssertNever< - Exclude -> +const RUN_STATUS_DESCRIPTION = + 'Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` is reported while the run is waiting at a human-in-the-loop pause point.' /** - * The list projection overlays `paused` onto the persisted status whenever the run has a - * `paused` or `partially_resumed` row in `paused_executions`. It cannot report `queued`: - * a run that is still only in the job queue has no log row to list. + * The list projection passes `workflow_execution_logs.status` through except where it + * overlays `paused` for a run holding a `paused` or `partially_resumed` row in + * `paused_executions`. Both branches land in the persisted set, so the reported enum is + * derived from it — a value missing here fails the response parse, and because list + * validation is whole-page one such row turns an entire page into a 500. `queued` is not + * reportable: a run still only in the job queue has no log row to list. */ -const V2_WORKFLOW_RUN_LIST_STATUSES = [...V2_PERSISTED_RUN_STATUSES, 'paused'] as const - -const RUN_STATUS_DESCRIPTION = - 'Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed.' - export const v2WorkflowRunListStatusValueSchema = z - .enum(V2_WORKFLOW_RUN_LIST_STATUSES) + .enum(PERSISTED_WORKFLOW_EXECUTION_STATUSES) .describe(RUN_STATUS_DESCRIPTION) /** @@ -1023,7 +1002,7 @@ export const v2WorkflowRunListStatusValueSchema = z * so a run accepted but not yet started reports `queued` rather than 404. */ export const v2WorkflowRunStatusValueSchema = z - .enum([...V2_WORKFLOW_RUN_LIST_STATUSES, 'queued']) + .enum([...PERSISTED_WORKFLOW_EXECUTION_STATUSES, 'queued']) .describe(RUN_STATUS_DESCRIPTION) /** diff --git a/apps/sim/lib/logs/types.ts b/apps/sim/lib/logs/types.ts index ebcc5531d20..fd4c1b18b41 100644 --- a/apps/sim/lib/logs/types.ts +++ b/apps/sim/lib/logs/types.ts @@ -204,13 +204,31 @@ export interface WorkflowExecutionLog { createdAt: string } +/** + * Every value written into `workflow_execution_logs.status`. The column is free text and + * one writer sets it through a raw `sql` CASE Drizzle cannot type-check, so this list — + * not the column type — is the only source of truth. API contracts that pass the column + * through derive their enums from it, so adding a status here widens the public wire; the + * contract tests fail until that widening is reviewed and the OpenAPI specs regenerated. + * + * `redacting` is transient while a finished run's output is scrubbed. `paused` is written + * only by `PauseResumeManager.markResumeAttemptFailed`, when a resume attempt does not run + * to completion — it failed admission, the run buffer was unavailable, the resume job could + * not be enqueued, or the attempt was cancelled. An ordinary human-in-the-loop pause + * persists `pending`. + */ +export const PERSISTED_WORKFLOW_EXECUTION_STATUSES = [ + 'pending', + 'running', + 'paused', + 'redacting', + 'completed', + 'failed', + 'cancelled', +] as const + export type PersistedWorkflowExecutionStatus = - | 'running' - | 'pending' - | 'completed' - | 'failed' - | 'cancelled' - | 'redacting' + (typeof PERSISTED_WORKFLOW_EXECUTION_STATUSES)[number] export interface CompletedWorkflowExecutionLog extends WorkflowExecutionLog { persistedStatus: PersistedWorkflowExecutionStatus diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 7023252251c..e80278a3085 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -411,7 +411,7 @@ export const workflowExecutionLogs = pgTable( ), level: text('level').notNull(), // 'info' | 'error' - status: text('status').notNull().default('running'), // 'running' | 'pending' | 'completed' | 'failed' | 'cancelled' + status: text('status').notNull().default('running'), // see PERSISTED_WORKFLOW_EXECUTION_STATUSES in apps/sim/lib/logs/types.ts trigger: text('trigger').notNull(), // 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' startedAt: timestamp('started_at').notNull(), From 16ba04d593483413ac2735cc6ab40c24462760d8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 12 Aug 2026 01:39:59 -0700 Subject: [PATCH 2/4] fix(v2): document both provenances of a reported paused run status --- apps/docs/openapi-v2-workflows.json | 4 ++-- apps/sim/lib/api/contracts/v2/workflows.ts | 5 +++-- packages/db/schema.ts | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/docs/openapi-v2-workflows.json b/apps/docs/openapi-v2-workflows.json index f8aadfc6d0e..3f784923e6d 100644 --- a/apps/docs/openapi-v2-workflows.json +++ b/apps/docs/openapi-v2-workflows.json @@ -3929,7 +3929,7 @@ "failed", "cancelled" ], - "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` is reported while the run is waiting at a human-in-the-loop pause point." + "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` covers two states a client may need to tell apart: the run is waiting at a human-in-the-loop pause point, or a resume attempt did not run to completion and the run is waiting to be resumed again (automatically when a retry is scheduled). Read the `paused` object on the single-run response to distinguish them." }, "trigger": { "type": "string", @@ -4070,7 +4070,7 @@ "cancelled", "queued" ], - "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` is reported while the run is waiting at a human-in-the-loop pause point." + "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` covers two states a client may need to tell apart: the run is waiting at a human-in-the-loop pause point, or a resume attempt did not run to completion and the run is waiting to be resumed again (automatically when a retry is scheduled). Read the `paused` object on the single-run response to distinguish them." }, "trigger": { "anyOf": [ diff --git a/apps/sim/lib/api/contracts/v2/workflows.ts b/apps/sim/lib/api/contracts/v2/workflows.ts index b7f594419dc..96ca7be3360 100644 --- a/apps/sim/lib/api/contracts/v2/workflows.ts +++ b/apps/sim/lib/api/contracts/v2/workflows.ts @@ -983,12 +983,13 @@ export const v2ResumeWorkflowContract = defineRouteContract({ }) const RUN_STATUS_DESCRIPTION = - 'Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` is reported while the run is waiting at a human-in-the-loop pause point.' + 'Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` covers two states a client may need to tell apart: the run is waiting at a human-in-the-loop pause point, or a resume attempt did not run to completion and the run is waiting to be resumed again (automatically when a retry is scheduled). Read the `paused` object on the single-run response to distinguish them.' /** * The list projection passes `workflow_execution_logs.status` through except where it * overlays `paused` for a run holding a `paused` or `partially_resumed` row in - * `paused_executions`. Both branches land in the persisted set, so the reported enum is + * `paused_executions` — so a reported `paused` is either that overlay or the persisted + * value a failed resume attempt left behind. Both branches land in the persisted set, so the reported enum is * derived from it — a value missing here fails the response parse, and because list * validation is whole-page one such row turns an entire page into a 500. `queued` is not * reportable: a run still only in the job queue has no log row to list. diff --git a/packages/db/schema.ts b/packages/db/schema.ts index e80278a3085..a000b0e81eb 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -411,7 +411,8 @@ export const workflowExecutionLogs = pgTable( ), level: text('level').notNull(), // 'info' | 'error' - status: text('status').notNull().default('running'), // see PERSISTED_WORKFLOW_EXECUTION_STATUSES in apps/sim/lib/logs/types.ts + /** See `PERSISTED_WORKFLOW_EXECUTION_STATUSES` in `apps/sim/lib/logs/types.ts`. */ + status: text('status').notNull().default('running'), trigger: text('trigger').notNull(), // 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' startedAt: timestamp('started_at').notNull(), From f6e0952565f091b09a72446f5d6f78512faa5def Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 12 Aug 2026 01:46:34 -0700 Subject: [PATCH 3/4] fix(v2): stop promising a paused discriminator the response cannot always provide --- apps/docs/openapi-v2-workflows.json | 4 ++-- apps/sim/lib/api/contracts/v2/workflows.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/docs/openapi-v2-workflows.json b/apps/docs/openapi-v2-workflows.json index 3f784923e6d..29ce5604454 100644 --- a/apps/docs/openapi-v2-workflows.json +++ b/apps/docs/openapi-v2-workflows.json @@ -3929,7 +3929,7 @@ "failed", "cancelled" ], - "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` covers two states a client may need to tell apart: the run is waiting at a human-in-the-loop pause point, or a resume attempt did not run to completion and the run is waiting to be resumed again (automatically when a retry is scheduled). Read the `paused` object on the single-run response to distinguish them." + "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` means the run is not executing and is waiting to be resumed: either held at a human-in-the-loop pause point, or left paused because a resume attempt did not run to completion. This field does not distinguish the two. On the single-run response a non-null `paused.automaticResumeWaitingReason` identifies a run waiting on a scheduled automatic retry, but it is cleared once retries are disabled, exhausted, or the failure is classified non-retryable — so its absence does not imply a human-input pause." }, "trigger": { "type": "string", @@ -4070,7 +4070,7 @@ "cancelled", "queued" ], - "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` covers two states a client may need to tell apart: the run is waiting at a human-in-the-loop pause point, or a resume attempt did not run to completion and the run is waiting to be resumed again (automatically when a retry is scheduled). Read the `paused` object on the single-run response to distinguish them." + "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` means the run is not executing and is waiting to be resumed: either held at a human-in-the-loop pause point, or left paused because a resume attempt did not run to completion. This field does not distinguish the two. On the single-run response a non-null `paused.automaticResumeWaitingReason` identifies a run waiting on a scheduled automatic retry, but it is cleared once retries are disabled, exhausted, or the failure is classified non-retryable — so its absence does not imply a human-input pause." }, "trigger": { "anyOf": [ diff --git a/apps/sim/lib/api/contracts/v2/workflows.ts b/apps/sim/lib/api/contracts/v2/workflows.ts index 96ca7be3360..056da517d91 100644 --- a/apps/sim/lib/api/contracts/v2/workflows.ts +++ b/apps/sim/lib/api/contracts/v2/workflows.ts @@ -983,7 +983,7 @@ export const v2ResumeWorkflowContract = defineRouteContract({ }) const RUN_STATUS_DESCRIPTION = - 'Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` covers two states a client may need to tell apart: the run is waiting at a human-in-the-loop pause point, or a resume attempt did not run to completion and the run is waiting to be resumed again (automatically when a retry is scheduled). Read the `paused` object on the single-run response to distinguish them.' + 'Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` means the run is not executing and is waiting to be resumed: either held at a human-in-the-loop pause point, or left paused because a resume attempt did not run to completion. This field does not distinguish the two. On the single-run response a non-null `paused.automaticResumeWaitingReason` identifies a run waiting on a scheduled automatic retry, but it is cleared once retries are disabled, exhausted, or the failure is classified non-retryable — so its absence does not imply a human-input pause.' /** * The list projection passes `workflow_execution_logs.status` through except where it From 4df9a7d1a5e8aba77f6bb42cc9c59f9eb0f71676 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 12 Aug 2026 01:50:37 -0700 Subject: [PATCH 4/4] fix(v2): describe the paused discriminator as the code actually records it --- apps/docs/openapi-v2-workflows.json | 4 ++-- apps/sim/lib/api/contracts/v2/workflows.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/docs/openapi-v2-workflows.json b/apps/docs/openapi-v2-workflows.json index 29ce5604454..722d9688c06 100644 --- a/apps/docs/openapi-v2-workflows.json +++ b/apps/docs/openapi-v2-workflows.json @@ -3929,7 +3929,7 @@ "failed", "cancelled" ], - "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` means the run is not executing and is waiting to be resumed: either held at a human-in-the-loop pause point, or left paused because a resume attempt did not run to completion. This field does not distinguish the two. On the single-run response a non-null `paused.automaticResumeWaitingReason` identifies a run waiting on a scheduled automatic retry, but it is cleared once retries are disabled, exhausted, or the failure is classified non-retryable — so its absence does not imply a human-input pause." + "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` means the run is not executing and is waiting to be resumed: either held at a human-in-the-loop pause point, or left paused because a resume attempt did not run to completion. The status alone does not say which. On the single-run response `paused.automaticResumeWaitingReason` distinguishes them: it is recorded whenever a resume attempt fails and cleared once a resume succeeds, so a null value means the run is waiting on human input. When the failure is not retryable or the automatic retries are exhausted, the reason is prefixed `Automatic resume requires manual intervention: `. Run-list items carry no `paused` object, so the two cases are indistinguishable there." }, "trigger": { "type": "string", @@ -4070,7 +4070,7 @@ "cancelled", "queued" ], - "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` means the run is not executing and is waiting to be resumed: either held at a human-in-the-loop pause point, or left paused because a resume attempt did not run to completion. This field does not distinguish the two. On the single-run response a non-null `paused.automaticResumeWaitingReason` identifies a run waiting on a scheduled automatic retry, but it is cleared once retries are disabled, exhausted, or the failure is classified non-retryable — so its absence does not imply a human-input pause." + "description": "Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` means the run is not executing and is waiting to be resumed: either held at a human-in-the-loop pause point, or left paused because a resume attempt did not run to completion. The status alone does not say which. On the single-run response `paused.automaticResumeWaitingReason` distinguishes them: it is recorded whenever a resume attempt fails and cleared once a resume succeeds, so a null value means the run is waiting on human input. When the failure is not retryable or the automatic retries are exhausted, the reason is prefixed `Automatic resume requires manual intervention: `. Run-list items carry no `paused` object, so the two cases are indistinguishable there." }, "trigger": { "anyOf": [ diff --git a/apps/sim/lib/api/contracts/v2/workflows.ts b/apps/sim/lib/api/contracts/v2/workflows.ts index 056da517d91..2eb1131dd71 100644 --- a/apps/sim/lib/api/contracts/v2/workflows.ts +++ b/apps/sim/lib/api/contracts/v2/workflows.ts @@ -983,7 +983,7 @@ export const v2ResumeWorkflowContract = defineRouteContract({ }) const RUN_STATUS_DESCRIPTION = - 'Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` means the run is not executing and is waiting to be resumed: either held at a human-in-the-loop pause point, or left paused because a resume attempt did not run to completion. This field does not distinguish the two. On the single-run response a non-null `paused.automaticResumeWaitingReason` identifies a run waiting on a scheduled automatic retry, but it is cleared once retries are disabled, exhausted, or the failure is classified non-retryable — so its absence does not imply a human-input pause.' + 'Current or terminal run status. `redacting` is transient, reported while the output of a finished run is being scrubbed. `paused` means the run is not executing and is waiting to be resumed: either held at a human-in-the-loop pause point, or left paused because a resume attempt did not run to completion. The status alone does not say which. On the single-run response `paused.automaticResumeWaitingReason` distinguishes them: it is recorded whenever a resume attempt fails and cleared once a resume succeeds, so a null value means the run is waiting on human input. When the failure is not retryable or the automatic retries are exhausted, the reason is prefixed `Automatic resume requires manual intervention: `. Run-list items carry no `paused` object, so the two cases are indistinguishable there.' /** * The list projection passes `workflow_execution_logs.status` through except where it