Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions apps/docs/content/docs/en/integrations/airtable.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ Write new records to an Airtable table
| `baseId` | string | Yes | Airtable base ID \(starts with "app", e.g., "appXXXXXXXXXXXXXX"\) |
| `tableId` | string | Yes | Table ID \(starts with "tbl"\) or table name |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hfhj

| `records` | json | Yes | Array of records to create, each with a `fields` object |
| `typecast` | boolean | No | When true, Airtable automatically converts string values to the field type |

#### Output

Expand All @@ -166,6 +167,7 @@ Update an existing record in an Airtable table by ID
| `tableId` | string | Yes | Table ID \(starts with "tbl"\) or table name |
| `recordId` | string | Yes | Record ID to update \(starts with "rec", e.g., "recXXXXXXXXXXXXXX"\) |
| `fields` | json | Yes | An object containing the field names and their new values |
| `typecast` | boolean | No | When true, Airtable automatically converts string values to the field type |

#### Output

Expand All @@ -190,6 +192,7 @@ Update multiple existing records in an Airtable table
| `baseId` | string | Yes | Airtable base ID \(starts with "app", e.g., "appXXXXXXXXXXXXXX"\) |
| `tableId` | string | Yes | Table ID \(starts with "tbl"\) or table name |
| `records` | json | Yes | Array of records to update, each with an `id` and a `fields` object |
| `typecast` | boolean | No | When true, Airtable automatically converts string values to the field type |

#### Output

Expand Down
6 changes: 5 additions & 1 deletion apps/sim/app/api/chat/manage/[id]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ describe('Chat Edit API Route', () => {
})

mockEncryptSecret.mockResolvedValue({ encrypted: 'encrypted-password' })
mockPerformFullDeploy.mockResolvedValue({ success: true, version: 1 })
mockPerformFullDeploy.mockResolvedValue({
success: true,
version: 1,
latestDeploymentAttempt: { status: 'active' },
})
mockNotifySocketDeploymentChanged.mockResolvedValue(undefined)
})

Expand Down
14 changes: 13 additions & 1 deletion apps/sim/app/api/chat/manage/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ export const PATCH = withRouteHandler(
const deployResult = await performFullDeploy({
workflowId: existingChat[0].workflowId,
userId: session.user.id,
request,
Comment thread
icecrasher321 marked this conversation as resolved.
})

if (!deployResult.success) {
Expand All @@ -153,6 +152,19 @@ export const PATCH = withRouteHandler(
: 500
return createErrorResponse(deployResult.error || 'Failed to redeploy workflow', status)
}
/**
* Deploys settle asynchronously: `success` only admits the attempt. The
* chat record must not advance until cutover finished, otherwise a later
* preparation failure strands the chat on the previous version with no
* error. Mirrors the gate in performChatDeploy.
*/
if (deployResult.latestDeploymentAttempt?.status !== 'active') {
return createErrorResponse(
deployResult.warnings?.[0] ??
'Workflow deployment is still preparing. Retry the chat update after it becomes active.',
409
)
}
logger.info(
`Redeployed workflow ${existingChat[0].workflowId} for chat update (v${deployResult.version})`
)
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/cron/renew-subscriptions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { verifyCronAuth } from '@/lib/auth/internal'
import { acquireLock, releaseLock } from '@/lib/core/config/redis'
import { runDetached } from '@/lib/core/utils/background'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { deliverableWebhookPredicate } from '@/lib/webhooks/delivery-predicate'
import { getCredentialOwner, getNotificationUrl } from '@/lib/webhooks/provider-subscription-utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'

Expand Down Expand Up @@ -96,7 +97,7 @@ async function renewExpiringSubscriptions(): Promise<{
.from(webhookTable)
.where(
and(
eq(webhookTable.isActive, true),
deliverableWebhookPredicate(webhookTable, 'active_only'),
or(
eq(webhookTable.provider, 'microsoft-teams'),
eq(webhookTable.provider, 'microsoftteams')
Expand Down
33 changes: 19 additions & 14 deletions apps/sim/app/api/mcp/serve/[serverId]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ describe('MCP Serve Route', () => {
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true, workspaceId: 'ws-1' }])
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])

hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValueOnce({
success: true,
Expand Down Expand Up @@ -307,7 +307,7 @@ describe('MCP Serve Route', () => {
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true, workspaceId: 'ws-1' }])
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])

hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValueOnce({
success: true,
Expand Down Expand Up @@ -361,7 +361,7 @@ describe('MCP Serve Route', () => {
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true, workspaceId: 'ws-1' }])
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])
mockGenerateInternalToken.mockResolvedValueOnce('internal-token-owner-1')
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ output: { ok: true } }), {
Expand Down Expand Up @@ -411,7 +411,9 @@ describe('MCP Serve Route', () => {
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true, workspaceId: workflowWorkspaceId }])
.mockResolvedValueOnce([
{ workspaceId: workflowWorkspaceId, deploymentVersionId: 'deployment-1' },
])

const req = new NextRequest('http://localhost:3000/api/mcp/serve/server-1', {
method: 'POST',
Expand Down Expand Up @@ -442,7 +444,7 @@ describe('MCP Serve Route', () => {
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true, workspaceId: 'ws-1' }])
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])
mockResolveBillingAttribution.mockResolvedValueOnce(
createBillingAttribution('different-actor', 'ws-1')
)
Expand Down Expand Up @@ -565,7 +567,7 @@ describe('MCP Serve Route', () => {
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true, workspaceId: 'ws-1' }])
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])
fetchMock.mockResolvedValueOnce(
new Response(
new ReadableStream<Uint8Array>({
Expand Down Expand Up @@ -609,7 +611,7 @@ describe('MCP Serve Route', () => {
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true, workspaceId: 'ws-1' }])
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])
fetchMock.mockResolvedValueOnce(
new Response(
new ReadableStream<Uint8Array>({
Expand Down Expand Up @@ -656,7 +658,7 @@ describe('MCP Serve Route', () => {
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true, workspaceId: 'ws-1' }])
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])
fetchMock.mockResolvedValueOnce(
new Response(
JSON.stringify({
Expand Down Expand Up @@ -689,6 +691,9 @@ describe('MCP Serve Route', () => {
const fetchOptions = fetchMock.mock.calls[0][1] as RequestInit
const headers = fetchOptions.headers as Record<string, string>
expect(headers['X-Sim-MCP-Tool-Call']).toBe('true')
expect(JSON.parse(fetchOptions.body as string)).toMatchObject({
deploymentVersionId: 'deployment-1',
})
})

it('preserves downstream attributed usage admission rejections', async () => {
Expand All @@ -703,7 +708,7 @@ describe('MCP Serve Route', () => {
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true, workspaceId: 'ws-1' }])
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])
fetchMock.mockResolvedValueOnce(
new Response(
JSON.stringify({
Expand Down Expand Up @@ -747,7 +752,7 @@ describe('MCP Serve Route', () => {
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true, workspaceId: 'ws-1' }])
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])
fetchMock.mockResolvedValueOnce(new Response('gateway timeout', { status: 408 }))

const req = new NextRequest('http://localhost:3000/api/mcp/serve/server-1', {
Expand Down Expand Up @@ -780,7 +785,7 @@ describe('MCP Serve Route', () => {
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true, workspaceId: 'ws-1' }])
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ success: true, output: false }), {
status: 200,
Expand Down Expand Up @@ -817,7 +822,7 @@ describe('MCP Serve Route', () => {
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true, workspaceId: 'ws-1' }])
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ success: true }), {
status: 200,
Expand Down Expand Up @@ -854,7 +859,7 @@ describe('MCP Serve Route', () => {
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true, workspaceId: 'ws-1' }])
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])
hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValueOnce({
success: true,
userId: 'user-1',
Expand Down Expand Up @@ -934,7 +939,7 @@ describe('MCP Serve Route', () => {
},
])
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true, workspaceId: 'ws-1' }])
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])
fetchMock.mockImplementationOnce((_url, init: RequestInit) => {
const signal = init.signal as AbortSignal
return new Promise<Response>((_resolve, reject) => {
Expand Down
29 changes: 26 additions & 3 deletions apps/sim/app/api/mcp/serve/[serverId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ import {
type Tool,
} from '@modelcontextprotocol/sdk/types.js'
import { db } from '@sim/db'
import { workflow, workflowMcpServer, workflowMcpTool, workspace } from '@sim/db/schema'
import {
workflow,
workflowDeploymentVersion,
workflowMcpServer,
workflowMcpTool,
workspace,
} from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, asc, eq, gt, isNull, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
Expand Down Expand Up @@ -751,14 +757,30 @@ async function handleToolsCall(
}

const [wf] = await db
.select({ isDeployed: workflow.isDeployed, workspaceId: workflow.workspaceId })
.select({
workspaceId: workflow.workspaceId,
deploymentVersionId: workflowDeploymentVersion.id,
})
.from(workflow)
.leftJoin(
workflowDeploymentVersion,
and(
eq(workflowDeploymentVersion.workflowId, workflow.id),
eq(workflowDeploymentVersion.isActive, true)
)
)
Comment thread
icecrasher321 marked this conversation as resolved.
.where(and(eq(workflow.id, tool.workflowId), isNull(workflow.archivedAt)))
.limit(1)
Comment thread
icecrasher321 marked this conversation as resolved.
const abortedAfterWorkflowLookup = callerAbortedJsonRpcResponse(id, abortSignal)
if (abortedAfterWorkflowLookup) return abortedAfterWorkflowLookup

if (!wf?.isDeployed) {
/**
* Deployed means an active version snapshot exists — the legacy
* `workflow.isDeployed` flag is not consulted because when the two
* disagree the workflow cannot serve traffic anyway. Same definition as
* the deploy status GET route.
*/
if (!wf?.deploymentVersionId) {
return NextResponse.json(
createError(id, ErrorCode.InternalError, 'Workflow is not deployed'),
{
Expand Down Expand Up @@ -813,6 +835,7 @@ async function handleToolsCall(
input: params.arguments || {},
triggerType: 'mcp',
includeFileBase64: false,
...(wf.deploymentVersionId ? { deploymentVersionId: wf.deploymentVersionId } : {}),
})
assertKnownSizeWithinLimit(
Buffer.byteLength(workflowRequestBody, 'utf-8'),
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/app/api/schedules/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ async function claimWorkflowSchedules(queuedAt: Date, limit: number) {
lastQueuedAt: workflowSchedule.lastQueuedAt,
timezone: workflowSchedule.timezone,
deploymentVersionId: workflowSchedule.deploymentVersionId,
deploymentOperationId: workflowSchedule.deploymentOperationId,
sourceType: workflowSchedule.sourceType,
})

Expand Down Expand Up @@ -864,6 +865,7 @@ async function processScheduleItem(
workspaceId,
billingAttribution,
deploymentVersionId: schedule.deploymentVersionId || undefined,
deploymentOperationId: schedule.deploymentOperationId || undefined,
cronExpression: schedule.cronExpression || undefined,
timezone: schedule.timezone || undefined,
lastRanAt: schedule.lastRanAt?.toISOString(),
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/api/tools/deployments/deploy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const result = await performFullDeploy({
workflowId,
userId: auth.userId,
workflowName: access.workflow.name || undefined,
versionName: name,
versionDescription: description ?? undefined,
requestId,
request,
})

if (!result.success) {
Expand All @@ -69,9 +67,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
success: true,
output: {
workflowId,
isDeployed: true,
isDeployed: Boolean(result.activeDeployment),
deployedAt: result.deployedAt?.toISOString() ?? null,
version: result.version,
activeDeployment: result.activeDeployment,
latestDeploymentAttempt: result.latestDeploymentAttempt,
warnings: result.warnings ?? [],
},
})
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/api/tools/deployments/promote/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
workflowId,
version,
userId: auth.userId,
workflow: access.workflow as Record<string, unknown>,
requestId,
request,
})

if (!result.success) {
Expand All @@ -69,9 +67,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
success: true,
output: {
workflowId,
isDeployed: true,
isDeployed: Boolean(result.activeDeployment),
deployedAt: result.deployedAt?.toISOString() ?? null,
version,
activeDeployment: result.activeDeployment,
latestDeploymentAttempt: result.latestDeploymentAttempt,
warnings: result.warnings ?? [],
},
})
Expand Down
20 changes: 20 additions & 0 deletions apps/sim/app/api/tools/deployments/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ describe('POST /api/tools/deployments/deploy', () => {
success: true,
deployedAt: new Date('2026-06-12T00:00:00Z'),
version: 4,
activeDeployment: {
deploymentVersionId: 'dv-4',
version: 4,
deployedAt: '2026-06-12T00:00:00.000Z',
},
})
})

Expand Down Expand Up @@ -157,6 +162,11 @@ describe('POST /api/tools/deployments/deploy', () => {
isDeployed: true,
deployedAt: '2026-06-12T00:00:00.000Z',
version: 4,
activeDeployment: {
deploymentVersionId: 'dv-4',
version: 4,
deployedAt: '2026-06-12T00:00:00.000Z',
},
warnings: [],
},
})
Expand Down Expand Up @@ -236,6 +246,11 @@ describe('POST /api/tools/deployments/promote', () => {
mockPerformActivateVersion.mockResolvedValue({
success: true,
deployedAt: new Date('2026-06-12T00:00:00Z'),
activeDeployment: {
deploymentVersionId: 'dv-3',
version: 3,
deployedAt: '2026-06-12T00:00:00.000Z',
},
})
})

Expand All @@ -255,6 +270,11 @@ describe('POST /api/tools/deployments/promote', () => {
isDeployed: true,
deployedAt: '2026-06-12T00:00:00.000Z',
version: 3,
activeDeployment: {
deploymentVersionId: 'dv-3',
version: 3,
deployedAt: '2026-06-12T00:00:00.000Z',
},
warnings: [],
})
})
Expand Down
Loading
Loading