Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
219a065
v0.4.12: guardrails, mistral models, privacy policy updates (#1608)
waleedlatif1 Oct 12, 2025
7f82ed3
v0.4.13: bugfixes for dev containers, posthog redirect, helm updates
icecrasher321 Oct 13, 2025
fb0fa1f
v0.4.14: canvas speedup and copilot context window
Sg312 Oct 14, 2025
2bc8c7b
v0.4.15: helm chart updates, telegram tools, youtube tools, file uplo…
waleedlatif1 Oct 15, 2025
04f109c
v0.4.16: executions dashboard, UI fixes, zep tools, slack fixes
icecrasher321 Oct 16, 2025
da091df
v0.4.17: input format + files support for webhooks, docs updates, das…
waleedlatif1 Oct 16, 2025
e4ddeb0
v0.4.18: file upload tools, copilot upgrade, docs changes, model filt…
icecrasher321 Oct 19, 2025
641e353
v0.4.19: landing page fix
icecrasher321 Oct 19, 2025
9751c9f
v0.4.20: internal request, kb url fixes, docs styling
icecrasher321 Oct 21, 2025
1b7437a
v0.4.21: more internal auth changes, supabase vector search tool
icecrasher321 Oct 22, 2025
71ae27b
v0.4.22: fix execution context pass for google sheets
icecrasher321 Oct 22, 2025
9b2490c
v0.4.23: webflow tools + triggers, copilot api key fix (#1723)
waleedlatif1 Oct 23, 2025
7f1ff7f
fix(billing): should allow restoring subscription (#1728)
icecrasher321 Oct 25, 2025
a02016e
v0.4.24: sso for chat deployment, usage indicator for file storage, m…
icecrasher321 Oct 27, 2025
9a4b9e2
v0.4.25: variables block, sort ordering for kb, careers page, storage…
waleedlatif1 Oct 29, 2025
f6e7891
Add pause resume block
Sg312 Nov 5, 2025
b994268
Add db schema
Sg312 Nov 5, 2025
bd06e6b
Initial test passes
Sg312 Nov 5, 2025
224b9b1
Tests pass
Sg312 Nov 5, 2025
99b27de
Execution pauses
Sg312 Nov 5, 2025
0f27f24
Snapshot serializer
Sg312 Nov 5, 2025
a7c3dea
Ui checkpoint
Sg312 Nov 5, 2025
dd0c1b6
Works 1
Sg312 Nov 5, 2025
ba9b46f
Pause resume simple v1
Sg312 Nov 5, 2025
cec668f
Hitl block works in parallel branches without timing overlap
Sg312 Nov 6, 2025
e28ee98
Pending status to logs
Sg312 Nov 6, 2025
2292661
Pause resume ui link
Sg312 Nov 6, 2025
517c5d3
Big context consolidation
Sg312 Nov 6, 2025
9b51546
HITL works in loops
Sg312 Nov 6, 2025
4871649
Fix parallels
Sg312 Nov 6, 2025
72ab4a0
Reference blocks properly
Sg312 Nov 6, 2025
be48de8
Fix tag dropdown and start block resolution
Sg312 Nov 6, 2025
a0b8dda
Filter console logs for hitl block
Sg312 Nov 6, 2025
c7f05f1
Fix notifs
Sg312 Nov 6, 2025
3df1e26
Fix logs page
Sg312 Nov 6, 2025
1e14a0c
Fix logs page again
Sg312 Nov 6, 2025
b4a2ef9
Fix
Sg312 Nov 6, 2025
d21ce2c
Merge remote-tracking branch 'origin' into feat/hitl-2
Sg312 Nov 6, 2025
01fbe7f
Checkpoint
Sg312 Nov 6, 2025
a29c443
Cleanup v1
Sg312 Nov 6, 2025
b390205
Refactor v2
Sg312 Nov 6, 2025
d40d899
Refactor v3
Sg312 Nov 6, 2025
07b1678
Refactor v4
Sg312 Nov 6, 2025
14a248f
Refactor v5
Sg312 Nov 6, 2025
1369be2
Resume page
Sg312 Nov 6, 2025
2f019b1
Fix variables in loops
Sg312 Nov 6, 2025
6449f77
Fix var res bugs
Sg312 Nov 6, 2025
2162026
Ui changes
Sg312 Nov 6, 2025
0b02471
Approval block
Sg312 Nov 6, 2025
d5f12ea
Hitl works e2e v1
Sg312 Nov 6, 2025
0180947
Merge with staging
Sg312 Nov 6, 2025
c64fe48
Fix tets
Sg312 Nov 6, 2025
b7026fe
Row level lock
Sg312 Nov 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions apps/sim/app/api/logs/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { db } from '@sim/db'
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
import { pausedExecutions, permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
import { and, desc, eq, gte, inArray, lte, type SQL, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
Expand Down Expand Up @@ -68,6 +68,9 @@ export async function GET(request: NextRequest) {
workflowWorkspaceId: workflow.workspaceId,
workflowCreatedAt: workflow.createdAt,
workflowUpdatedAt: workflow.updatedAt,
pausedStatus: pausedExecutions.status,
pausedTotalPauseCount: pausedExecutions.totalPauseCount,
pausedResumedCount: pausedExecutions.resumedCount,
}
: {
// Basic mode - exclude large fields for better performance
Expand All @@ -92,11 +95,18 @@ export async function GET(request: NextRequest) {
workflowWorkspaceId: workflow.workspaceId,
workflowCreatedAt: workflow.createdAt,
workflowUpdatedAt: workflow.updatedAt,
pausedStatus: pausedExecutions.status,
pausedTotalPauseCount: pausedExecutions.totalPauseCount,
pausedResumedCount: pausedExecutions.resumedCount,
}

const baseQuery = db
.select(selectColumns)
.from(workflowExecutionLogs)
.leftJoin(
pausedExecutions,
eq(pausedExecutions.executionId, workflowExecutionLogs.executionId)
)
.innerJoin(
workflow,
and(
Expand Down Expand Up @@ -186,6 +196,10 @@ export async function GET(request: NextRequest) {
const countQuery = db
.select({ count: sql<number>`count(*)` })
.from(workflowExecutionLogs)
.leftJoin(
pausedExecutions,
eq(pausedExecutions.executionId, workflowExecutionLogs.executionId)
)
.innerJoin(
workflow,
and(
Expand Down Expand Up @@ -340,13 +354,18 @@ export async function GET(request: NextRequest) {
return {
id: log.id,
workflowId: log.workflowId,
executionId: params.details === 'full' ? log.executionId : undefined,
executionId: log.executionId,
level: log.level,
duration: log.totalDurationMs ? `${log.totalDurationMs}ms` : null,
trigger: log.trigger,
createdAt: log.startedAt.toISOString(),
files: params.details === 'full' ? log.files || undefined : undefined,
workflow: workflowSummary,
pauseSummary: {
status: log.pausedStatus ?? null,
total: log.pausedTotalPauseCount ?? 0,
resumed: log.pausedResumedCount ?? 0,
},
executionData:
params.details === 'full'
? {
Expand All @@ -361,6 +380,10 @@ export async function GET(request: NextRequest) {
params.details === 'full'
? (costSummary as any)
: { total: (costSummary as any)?.total || 0 },
hasPendingPause:
(Number(log.pausedTotalPauseCount ?? 0) > 0 &&
Number(log.pausedResumedCount ?? 0) < Number(log.pausedTotalPauseCount ?? 0)) ||
(log.pausedStatus && log.pausedStatus !== 'fully_resumed'),
}
})
return NextResponse.json(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { PauseResumeManager } from '@/lib/workflows/executor/pause-resume-manager'
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'

const logger = createLogger('WorkflowResumeAPI')

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

export async function POST(
request: NextRequest,
{
params,
}: {
params: Promise<{ workflowId: string; executionId: string; contextId: string }>
}
) {
const { workflowId, executionId, contextId } = await params

const access = await validateWorkflowAccess(request, workflowId, false)
if (access.error) {
return NextResponse.json({ error: access.error.message }, { status: access.error.status })
}

const workflow = access.workflow!

let payload: any = {}
try {
payload = await request.json()
} catch {
payload = {}
}

const resumeInput = payload?.input ?? payload ?? {}
const userId = workflow.userId ?? ''

try {
const enqueueResult = await PauseResumeManager.enqueueOrStartResume({
executionId,
contextId,
resumeInput,
userId,
})

if (enqueueResult.status === 'queued') {
return NextResponse.json({
status: 'queued',
executionId: enqueueResult.resumeExecutionId,
queuePosition: enqueueResult.queuePosition,
message: 'Resume queued. It will run after current resumes finish.',
})
}

PauseResumeManager.startResumeExecution({
resumeEntryId: enqueueResult.resumeEntryId,
resumeExecutionId: enqueueResult.resumeExecutionId,
pausedExecution: enqueueResult.pausedExecution,
contextId: enqueueResult.contextId,
resumeInput: enqueueResult.resumeInput,
userId: enqueueResult.userId,
}).catch((error) => {
logger.error('Failed to start resume execution', {
workflowId,
parentExecutionId: executionId,
resumeExecutionId: enqueueResult.resumeExecutionId,
error,
})
})

return NextResponse.json({
status: 'started',
executionId: enqueueResult.resumeExecutionId,
message: 'Resume execution started.',
})
} catch (error: any) {
logger.error('Resume request failed', {
workflowId,
executionId,
contextId,
error,
})
return NextResponse.json(
{ error: error.message || 'Failed to queue resume request' },
{ status: 400 }
)
}
}

export async function GET(
request: NextRequest,
{
params,
}: {
params: Promise<{ workflowId: string; executionId: string; contextId: string }>
}
) {
const { workflowId, executionId, contextId } = await params

const access = await validateWorkflowAccess(request, workflowId, false)
if (access.error) {
return NextResponse.json({ error: access.error.message }, { status: access.error.status })
}

const detail = await PauseResumeManager.getPauseContextDetail({
workflowId,
executionId,
contextId,
})

if (!detail) {
return NextResponse.json({ error: 'Pause context not found' }, { status: 404 })
}

return NextResponse.json(detail)
}
48 changes: 48 additions & 0 deletions apps/sim/app/api/resume/[workflowId]/[executionId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { PauseResumeManager } from '@/lib/workflows/executor/pause-resume-manager'
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'

const logger = createLogger('WorkflowResumeExecutionAPI')

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

export async function GET(
request: NextRequest,
{
params,
}: {
params: Promise<{ workflowId: string; executionId: string }>
}
) {
const { workflowId, executionId } = await params

const access = await validateWorkflowAccess(request, workflowId, false)
if (access.error) {
return NextResponse.json({ error: access.error.message }, { status: access.error.status })
}

try {
const detail = await PauseResumeManager.getPausedExecutionDetail({
workflowId,
executionId,
})

if (!detail) {
return NextResponse.json({ error: 'Paused execution not found' }, { status: 404 })
}

return NextResponse.json(detail)
} catch (error: any) {
logger.error('Failed to load paused execution detail', {
workflowId,
executionId,
error,
})
return NextResponse.json(
{ error: error?.message || 'Failed to load paused execution detail' },
{ status: 500 }
)
}
}
37 changes: 37 additions & 0 deletions apps/sim/app/api/workflows/[id]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '@/lib/workflows/db-helpers'
import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
import { type ExecutionEvent, encodeSSEEvent } from '@/lib/workflows/executor/execution-events'
import { PauseResumeManager } from '@/lib/workflows/executor/pause-resume-manager'
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
import { type ExecutionMetadata, ExecutionSnapshot } from '@/executor/execution/snapshot'
import type { StreamingExecution } from '@/executor/types'
Expand Down Expand Up @@ -135,6 +136,24 @@ export async function executeWorkflow(
loggingSession,
})

if (result.status === 'paused') {
if (!result.snapshotSeed) {
logger.error(`[${requestId}] Missing snapshot seed for paused execution`, {
executionId,
})
} else {
await PauseResumeManager.persistPauseResult({
workflowId,
executionId,
pausePoints: result.pausePoints || [],
snapshotSeed: result.snapshotSeed,
executorUserId: result.metadata?.userId,
})
}
} else {
await PauseResumeManager.processQueuedResumes(executionId)
}

if (streamConfig?.skipLoggingComplete) {
return {
...result,
Expand Down Expand Up @@ -605,6 +624,24 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
loggingSession,
})

if (result.status === 'paused') {
if (!result.snapshotSeed) {
logger.error(`[${requestId}] Missing snapshot seed for paused execution`, {
executionId,
})
} else {
await PauseResumeManager.persistPauseResult({
workflowId,
executionId,
pausePoints: result.pausePoints || [],
snapshotSeed: result.snapshotSeed,
executorUserId: result.metadata?.userId,
})
}
} else {
await PauseResumeManager.processQueuedResumes(executionId)
}

if (result.error === 'Workflow execution was cancelled') {
logger.info(`[${requestId}] Workflow execution was cancelled`)
sendEvent({
Expand Down
34 changes: 34 additions & 0 deletions apps/sim/app/api/workflows/[id]/paused/[executionId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { type NextRequest, NextResponse } from 'next/server'
import { PauseResumeManager } from '@/lib/workflows/executor/pause-resume-manager'
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

export async function GET(
request: NextRequest,
{
params,
}: {
params: { id: string; executionId: string }
}
) {
const workflowId = params.id
const executionId = params.executionId

const access = await validateWorkflowAccess(request, workflowId, false)
if (access.error) {
return NextResponse.json({ error: access.error.message }, { status: access.error.status })
}

const detail = await PauseResumeManager.getPausedExecutionDetail({
workflowId,
executionId,
})

if (!detail) {
return NextResponse.json({ error: 'Paused execution not found' }, { status: 404 })
}

return NextResponse.json(detail)
}
31 changes: 31 additions & 0 deletions apps/sim/app/api/workflows/[id]/paused/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { type NextRequest, NextResponse } from 'next/server'
import { PauseResumeManager } from '@/lib/workflows/executor/pause-resume-manager'
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

export async function GET(
request: NextRequest,
{
params,
}: {
params: { id: string }
}
) {
const workflowId = params.id

const access = await validateWorkflowAccess(request, workflowId, false)
if (access.error) {
return NextResponse.json({ error: access.error.message }, { status: access.error.status })
}

const statusFilter = request.nextUrl.searchParams.get('status') || undefined

const pausedExecutions = await PauseResumeManager.listPausedExecutions({
workflowId,
status: statusFilter,
})

return NextResponse.json({ pausedExecutions })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { redirect } from 'next/navigation'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

interface PageParams {
workflowId: string
executionId: string
contextId: string
}

export default async function ResumePage({ params }: { params: Promise<PageParams> }) {
const { workflowId, executionId, contextId } = await params
redirect(`/resume/${workflowId}/${executionId}?contextId=${contextId}`)
}
Loading