From 20cc0185bfedf10b47ef89b93b41b4ba1f1184e5 Mon Sep 17 00:00:00 2001
From: Waleed
Date: Sat, 11 Apr 2026 11:22:50 -0700
Subject: [PATCH 1/7] fix(execution): fix isolated-vm memory leak and add
worker recycling (#4108)
* fix(execution): fix isolated-vm memory leak and add worker recycling
* fix(execution): mirror retirement check in send-failure path and fix pool sizing
* chore(execution): remove verbose comments from isolated-vm changes
* fix(execution): apply retiring-worker exclusion to drainQueue pool size check
* fix(execution): increment lifetimeExecutions on parent-side timeout
---
apps/sim/lib/core/config/env.ts | 1 +
apps/sim/lib/execution/isolated-vm-worker.cjs | 53 +++++++++++++++----
apps/sim/lib/execution/isolated-vm.ts | 49 ++++++++++++++---
3 files changed, 87 insertions(+), 16 deletions(-)
diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts
index f4c751b7b72..7b329a2fd12 100644
--- a/apps/sim/lib/core/config/env.ts
+++ b/apps/sim/lib/core/config/env.ts
@@ -235,6 +235,7 @@ export const env = createEnv({
IVM_DISTRIBUTED_MAX_INFLIGHT_PER_OWNER:z.string().optional().default('2200'), // Max owner in-flight leases across replicas
IVM_DISTRIBUTED_LEASE_MIN_TTL_MS: z.string().optional().default('120000'), // Min TTL for distributed in-flight leases (ms)
IVM_QUEUE_TIMEOUT_MS: z.string().optional().default('300000'), // Max queue wait before rejection (ms)
+ IVM_MAX_EXECUTIONS_PER_WORKER: z.string().optional().default('500'), // Max lifetime executions before worker is recycled
// Knowledge Base Processing Configuration - Shared across all processing methods
KB_CONFIG_MAX_DURATION: z.number().optional().default(600), // Max processing duration in seconds (10 minutes)
diff --git a/apps/sim/lib/execution/isolated-vm-worker.cjs b/apps/sim/lib/execution/isolated-vm-worker.cjs
index 2641b80e11d..a6f25053267 100644
--- a/apps/sim/lib/execution/isolated-vm-worker.cjs
+++ b/apps/sim/lib/execution/isolated-vm-worker.cjs
@@ -142,27 +142,40 @@ async function executeCode(request) {
stdoutTruncated = true
}
+ let context = null
+ let bootstrapScript = null
+ let userScript = null
+ let logCallback = null
+ let errorCallback = null
+ let fetchCallback = null
+ const externalCopies = []
+
try {
isolate = new ivm.Isolate({ memoryLimit: 128 })
- const context = await isolate.createContext()
+ context = await isolate.createContext()
const jail = context.global
await jail.set('global', jail.derefInto())
- const logCallback = new ivm.Callback((...args) => {
+ logCallback = new ivm.Callback((...args) => {
const message = args.map((arg) => stringifyLogValue(arg)).join(' ')
appendStdout(`${message}\n`)
})
await jail.set('__log', logCallback)
- const errorCallback = new ivm.Callback((...args) => {
+ errorCallback = new ivm.Callback((...args) => {
const message = args.map((arg) => stringifyLogValue(arg)).join(' ')
appendStdout(`ERROR: ${message}\n`)
})
await jail.set('__error', errorCallback)
- await jail.set('params', new ivm.ExternalCopy(params).copyInto())
- await jail.set('environmentVariables', new ivm.ExternalCopy(envVars).copyInto())
+ const paramsCopy = new ivm.ExternalCopy(params)
+ externalCopies.push(paramsCopy)
+ await jail.set('params', paramsCopy.copyInto())
+
+ const envVarsCopy = new ivm.ExternalCopy(envVars)
+ externalCopies.push(envVarsCopy)
+ await jail.set('environmentVariables', envVarsCopy.copyInto())
for (const [key, value] of Object.entries(contextVariables)) {
if (value === undefined) {
@@ -170,11 +183,13 @@ async function executeCode(request) {
} else if (value === null) {
await jail.set(key, null)
} else {
- await jail.set(key, new ivm.ExternalCopy(value).copyInto())
+ const ctxCopy = new ivm.ExternalCopy(value)
+ externalCopies.push(ctxCopy)
+ await jail.set(key, ctxCopy.copyInto())
}
}
- const fetchCallback = new ivm.Reference(async (url, optionsJson) => {
+ fetchCallback = new ivm.Reference(async (url, optionsJson) => {
return new Promise((resolve) => {
const fetchId = ++fetchIdCounter
const timeout = setTimeout(() => {
@@ -267,7 +282,7 @@ async function executeCode(request) {
}
`
- const bootstrapScript = await isolate.compileScript(bootstrap)
+ bootstrapScript = await isolate.compileScript(bootstrap)
await bootstrapScript.run(context)
const wrappedCode = `
@@ -290,7 +305,7 @@ async function executeCode(request) {
})()
`
- const userScript = await isolate.compileScript(wrappedCode, { filename: 'user-function.js' })
+ userScript = await isolate.compileScript(wrappedCode, { filename: 'user-function.js' })
const resultJson = await userScript.run(context, { timeout: timeoutMs, promise: true })
let result = null
@@ -357,8 +372,26 @@ async function executeCode(request) {
},
}
} finally {
+ const releaseables = [
+ userScript,
+ bootstrapScript,
+ ...externalCopies,
+ fetchCallback,
+ errorCallback,
+ logCallback,
+ context,
+ ]
+ for (const obj of releaseables) {
+ if (obj) {
+ try {
+ obj.release()
+ } catch {}
+ }
+ }
if (isolate) {
- isolate.dispose()
+ try {
+ isolate.dispose()
+ } catch {}
}
}
}
diff --git a/apps/sim/lib/execution/isolated-vm.ts b/apps/sim/lib/execution/isolated-vm.ts
index 877035760e5..3ec9b30c938 100644
--- a/apps/sim/lib/execution/isolated-vm.ts
+++ b/apps/sim/lib/execution/isolated-vm.ts
@@ -70,6 +70,7 @@ const DISTRIBUTED_MAX_INFLIGHT_PER_OWNER =
Number.parseInt(env.IVM_DISTRIBUTED_MAX_INFLIGHT_PER_OWNER) ||
MAX_ACTIVE_PER_OWNER + MAX_QUEUED_PER_OWNER
const DISTRIBUTED_LEASE_MIN_TTL_MS = Number.parseInt(env.IVM_DISTRIBUTED_LEASE_MIN_TTL_MS) || 120000
+const MAX_EXECUTIONS_PER_WORKER = Number.parseInt(env.IVM_MAX_EXECUTIONS_PER_WORKER) || 500
const DISTRIBUTED_KEY_PREFIX = 'ivm:fair:v1:owner'
const LEASE_REDIS_DEADLINE_MS = 200
const QUEUE_RETRY_DELAY_MS = 1000
@@ -89,6 +90,8 @@ interface WorkerInfo {
pendingExecutions: Map
idleTimeout: ReturnType | null
id: number
+ lifetimeExecutions: number
+ retiring: boolean
}
interface QueuedExecution {
@@ -538,8 +541,20 @@ function handleWorkerMessage(workerId: number, message: unknown) {
owner.activeExecutions = Math.max(0, owner.activeExecutions - 1)
maybeCleanupOwner(owner.ownerKey)
}
+ workerInfo!.lifetimeExecutions++
+ if (workerInfo!.lifetimeExecutions >= MAX_EXECUTIONS_PER_WORKER && !workerInfo!.retiring) {
+ workerInfo!.retiring = true
+ logger.info('Worker marked for retirement', {
+ workerId,
+ lifetimeExecutions: workerInfo!.lifetimeExecutions,
+ })
+ }
+ if (workerInfo!.retiring && workerInfo!.activeExecutions === 0) {
+ cleanupWorker(workerId)
+ } else {
+ resetWorkerIdleTimeout(workerId)
+ }
pending.resolve(msg.result as IsolatedVMExecutionResult)
- resetWorkerIdleTimeout(workerId)
drainQueue()
}
return
@@ -679,6 +694,8 @@ function spawnWorker(): Promise {
pendingExecutions: new Map(),
idleTimeout: null,
id: workerId,
+ lifetimeExecutions: 0,
+ retiring: false,
}
workerInfo.readyPromise = new Promise((resolve, reject) => {
@@ -710,7 +727,8 @@ function spawnWorker(): Promise {
import('node:child_process')
.then(({ spawn }) => {
- const proc = spawn('node', [workerPath], {
+ // Required for isolated-vm on Node.js 20+ (issue #377)
+ const proc = spawn('node', ['--no-node-snapshot', workerPath], {
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
serialization: 'json',
})
@@ -801,6 +819,7 @@ function selectWorker(): WorkerInfo | null {
let best: WorkerInfo | null = null
for (const w of workers.values()) {
if (!w.ready) continue
+ if (w.retiring) continue
if (w.activeExecutions >= MAX_PER_WORKER) continue
if (!best || w.activeExecutions < best.activeExecutions) {
best = w
@@ -818,7 +837,8 @@ async function acquireWorker(): Promise {
const existing = selectWorker()
if (existing) return existing
- const currentPoolSize = workers.size + spawnInProgress
+ const activeWorkerCount = [...workers.values()].filter((w) => !w.retiring).length
+ const currentPoolSize = activeWorkerCount + spawnInProgress
if (currentPoolSize < POOL_SIZE) {
try {
return await spawnWorker()
@@ -850,12 +870,24 @@ function dispatchToWorker(
totalActiveExecutions--
ownerState.activeExecutions = Math.max(0, ownerState.activeExecutions - 1)
maybeCleanupOwner(ownerState.ownerKey)
+ workerInfo.lifetimeExecutions++
+ if (workerInfo.lifetimeExecutions >= MAX_EXECUTIONS_PER_WORKER && !workerInfo.retiring) {
+ workerInfo.retiring = true
+ logger.info('Worker marked for retirement', {
+ workerId: workerInfo.id,
+ lifetimeExecutions: workerInfo.lifetimeExecutions,
+ })
+ }
resolve({
result: null,
stdout: '',
error: { message: `Execution timed out after ${req.timeoutMs}ms`, name: 'TimeoutError' },
})
- resetWorkerIdleTimeout(workerInfo.id)
+ if (workerInfo.retiring && workerInfo.activeExecutions === 0) {
+ cleanupWorker(workerInfo.id)
+ } else {
+ resetWorkerIdleTimeout(workerInfo.id)
+ }
drainQueue()
}, req.timeoutMs + 1000)
@@ -878,7 +910,11 @@ function dispatchToWorker(
stdout: '',
error: { message: 'Code execution failed to start. Please try again.', name: 'Error' },
})
- resetWorkerIdleTimeout(workerInfo.id)
+ if (workerInfo.retiring && workerInfo.activeExecutions === 0) {
+ cleanupWorker(workerInfo.id)
+ } else {
+ resetWorkerIdleTimeout(workerInfo.id)
+ }
// Defer to break synchronous recursion: drainQueue → dispatchToWorker → catch → drainQueue
queueMicrotask(() => drainQueue())
}
@@ -952,7 +988,8 @@ function drainQueue() {
while (queueLength() > 0 && totalActiveExecutions < MAX_CONCURRENT) {
const worker = selectWorker()
if (!worker) {
- const currentPoolSize = workers.size + spawnInProgress
+ const activeWorkerCount = [...workers.values()].filter((w) => !w.retiring).length
+ const currentPoolSize = activeWorkerCount + spawnInProgress
if (currentPoolSize < POOL_SIZE) {
spawnWorker()
.then(() => drainQueue())
From c8525852d43d095b09096976ac03defe806a9101 Mon Sep 17 00:00:00 2001
From: Waleed
Date: Sat, 11 Apr 2026 11:41:23 -0700
Subject: [PATCH 2/7] chore(triggers): deprecate trigger-save subblock (#4107)
* chore(triggers): deprecate trigger-save subblock
Remove the defunct triggerSave subblock from all 102 trigger definitions,
the SubBlockType union, SYSTEM_SUBBLOCK_IDS, tool params, and command
templates. Retain the backwards-compat filter in getTrigger() for any
legacy stored data.
* fix(triggers): remove leftover no-op blocks.push() in linear utils
* chore(triggers): remove orphaned triggerId property and stale comments
---
.claude/commands/add-trigger.md | 2 --
.cursor/commands/add-trigger.md | 2 --
apps/sim/blocks/blocks.test.ts | 1 -
apps/sim/blocks/blocks/linear.ts | 1 -
apps/sim/blocks/types.ts | 6 ------
apps/sim/tools/params.ts | 1 -
apps/sim/triggers/airtable/webhook.ts | 8 --------
apps/sim/triggers/attio/utils.ts | 7 -------
apps/sim/triggers/calendly/invitee_canceled.ts | 12 ------------
apps/sim/triggers/calendly/invitee_created.ts | 12 ------------
apps/sim/triggers/calendly/routing_form_submitted.ts | 12 ------------
apps/sim/triggers/calendly/webhook.ts | 12 ------------
apps/sim/triggers/circleback/meeting_completed.ts | 12 ------------
apps/sim/triggers/circleback/meeting_notes.ts | 12 ------------
apps/sim/triggers/circleback/webhook.ts | 12 ------------
apps/sim/triggers/constants.ts | 1 -
apps/sim/triggers/fathom/new_meeting.ts | 12 ------------
apps/sim/triggers/fathom/webhook.ts | 12 ------------
.../sim/triggers/fireflies/transcription_complete.ts | 8 --------
apps/sim/triggers/generic/webhook.ts | 8 --------
apps/sim/triggers/github/issue_closed.ts | 12 ------------
apps/sim/triggers/github/issue_comment.ts | 12 ------------
apps/sim/triggers/github/issue_opened.ts | 12 ------------
apps/sim/triggers/github/pr_closed.ts | 12 ------------
apps/sim/triggers/github/pr_comment.ts | 12 ------------
apps/sim/triggers/github/pr_merged.ts | 12 ------------
apps/sim/triggers/github/pr_opened.ts | 12 ------------
apps/sim/triggers/github/pr_reviewed.ts | 12 ------------
apps/sim/triggers/github/push.ts | 12 ------------
apps/sim/triggers/github/release_published.ts | 12 ------------
apps/sim/triggers/github/webhook.ts | 12 ------------
apps/sim/triggers/github/workflow_run.ts | 12 ------------
apps/sim/triggers/gmail/poller.ts | 8 --------
apps/sim/triggers/google-calendar/poller.ts | 8 --------
apps/sim/triggers/google-drive/poller.ts | 8 --------
apps/sim/triggers/google-sheets/poller.ts | 8 --------
apps/sim/triggers/googleforms/webhook.ts | 8 --------
apps/sim/triggers/grain/highlight_created.ts | 12 ------------
apps/sim/triggers/grain/highlight_updated.ts | 12 ------------
apps/sim/triggers/grain/item_added.ts | 12 ------------
apps/sim/triggers/grain/item_updated.ts | 12 ------------
apps/sim/triggers/grain/recording_created.ts | 12 ------------
apps/sim/triggers/grain/recording_updated.ts | 12 ------------
apps/sim/triggers/grain/story_created.ts | 12 ------------
apps/sim/triggers/grain/webhook.ts | 12 ------------
apps/sim/triggers/hubspot/company_created.ts | 11 -----------
apps/sim/triggers/hubspot/company_deleted.ts | 11 -----------
apps/sim/triggers/hubspot/company_merged.ts | 11 -----------
.../sim/triggers/hubspot/company_property_changed.ts | 11 -----------
apps/sim/triggers/hubspot/company_restored.ts | 11 -----------
apps/sim/triggers/hubspot/contact_created.ts | 11 -----------
apps/sim/triggers/hubspot/contact_deleted.ts | 11 -----------
apps/sim/triggers/hubspot/contact_merged.ts | 11 -----------
apps/sim/triggers/hubspot/contact_privacy_deleted.ts | 11 -----------
.../sim/triggers/hubspot/contact_property_changed.ts | 11 -----------
apps/sim/triggers/hubspot/contact_restored.ts | 11 -----------
apps/sim/triggers/hubspot/conversation_creation.ts | 11 -----------
apps/sim/triggers/hubspot/conversation_deletion.ts | 11 -----------
.../sim/triggers/hubspot/conversation_new_message.ts | 11 -----------
.../hubspot/conversation_privacy_deletion.ts | 11 -----------
.../hubspot/conversation_property_changed.ts | 11 -----------
apps/sim/triggers/hubspot/deal_created.ts | 11 -----------
apps/sim/triggers/hubspot/deal_deleted.ts | 11 -----------
apps/sim/triggers/hubspot/deal_merged.ts | 11 -----------
apps/sim/triggers/hubspot/deal_property_changed.ts | 11 -----------
apps/sim/triggers/hubspot/deal_restored.ts | 11 -----------
apps/sim/triggers/hubspot/ticket_created.ts | 11 -----------
apps/sim/triggers/hubspot/ticket_deleted.ts | 11 -----------
apps/sim/triggers/hubspot/ticket_merged.ts | 11 -----------
apps/sim/triggers/hubspot/ticket_property_changed.ts | 11 -----------
apps/sim/triggers/hubspot/ticket_restored.ts | 11 -----------
apps/sim/triggers/hubspot/webhook.ts | 11 -----------
apps/sim/triggers/imap/poller.ts | 8 --------
apps/sim/triggers/index.ts | 8 +++++---
apps/sim/triggers/jira/issue_commented.ts | 12 ------------
apps/sim/triggers/jira/issue_created.ts | 12 ------------
apps/sim/triggers/jira/issue_deleted.ts | 12 ------------
apps/sim/triggers/jira/issue_updated.ts | 12 ------------
apps/sim/triggers/jira/webhook.ts | 12 ------------
apps/sim/triggers/jira/worklog_created.ts | 12 ------------
apps/sim/triggers/linear/comment_created.ts | 12 ------------
apps/sim/triggers/linear/comment_updated.ts | 12 ------------
apps/sim/triggers/linear/customer_request_created.ts | 12 ------------
apps/sim/triggers/linear/customer_request_updated.ts | 12 ------------
apps/sim/triggers/linear/cycle_created.ts | 12 ------------
apps/sim/triggers/linear/cycle_updated.ts | 12 ------------
apps/sim/triggers/linear/issue_created.ts | 12 ------------
apps/sim/triggers/linear/issue_removed.ts | 12 ------------
apps/sim/triggers/linear/issue_updated.ts | 12 ------------
apps/sim/triggers/linear/label_created.ts | 12 ------------
apps/sim/triggers/linear/label_updated.ts | 12 ------------
apps/sim/triggers/linear/project_created.ts | 12 ------------
apps/sim/triggers/linear/project_update_created.ts | 12 ------------
apps/sim/triggers/linear/project_updated.ts | 12 ------------
apps/sim/triggers/linear/utils.ts | 12 +-----------
apps/sim/triggers/linear/webhook.ts | 12 ------------
apps/sim/triggers/microsoftteams/chat_webhook.ts | 12 ------------
apps/sim/triggers/microsoftteams/webhook.ts | 12 ------------
apps/sim/triggers/outlook/poller.ts | 8 --------
apps/sim/triggers/rss/poller.ts | 8 --------
apps/sim/triggers/slack/webhook.ts | 8 --------
apps/sim/triggers/stripe/webhook.ts | 8 --------
apps/sim/triggers/telegram/webhook.ts | 8 --------
apps/sim/triggers/twilio_voice/webhook.ts | 8 --------
apps/sim/triggers/typeform/webhook.ts | 8 --------
apps/sim/triggers/webflow/collection_item_changed.ts | 12 ------------
apps/sim/triggers/webflow/collection_item_created.ts | 12 ------------
apps/sim/triggers/webflow/collection_item_deleted.ts | 12 ------------
apps/sim/triggers/webflow/form_submission.ts | 12 ------------
apps/sim/triggers/whatsapp/webhook.ts | 8 --------
bun.lock | 1 +
111 files changed, 7 insertions(+), 1140 deletions(-)
diff --git a/.claude/commands/add-trigger.md b/.claude/commands/add-trigger.md
index e12eb393ba7..f5990517573 100644
--- a/.claude/commands/add-trigger.md
+++ b/.claude/commands/add-trigger.md
@@ -423,7 +423,6 @@ export const {service}PollingTrigger: TriggerConfig = {
subBlocks: [
{ id: 'triggerCredentials', type: 'oauth-input', title: 'Credentials', serviceId: '{service}', requiredScopes: [], required: true, mode: 'trigger', supportsCredentialSets: true },
// ... service-specific config fields (dropdowns, inputs, switches) ...
- { id: 'triggerSave', type: 'trigger-save', title: '', hideFromPreview: true, mode: 'trigger', triggerId: '{service}_poller' },
{ id: 'triggerInstructions', type: 'text', title: 'Setup Instructions', hideFromPreview: true, mode: 'trigger', defaultValue: '...' },
],
@@ -486,7 +485,6 @@ Add to `helm/sim/values.yaml` under the existing polling cron jobs:
- [ ] Handler implements `PollingProviderHandler` at `lib/webhooks/polling/{service}.ts`
- [ ] Trigger config has `polling: true` and defines subBlocks manually (no `buildTriggerSubBlocks`)
- [ ] Provider string matches across: trigger config, handler, `POLLING_PROVIDERS`, polling registry
-- [ ] `triggerSave` subBlock `triggerId` matches trigger config `id`
- [ ] First poll seeds state and emits nothing
- [ ] Added provider to `POLLING_PROVIDERS` in `triggers/constants.ts`
- [ ] Added handler to `POLLING_HANDLERS` in `lib/webhooks/polling/registry.ts`
diff --git a/.cursor/commands/add-trigger.md b/.cursor/commands/add-trigger.md
index ae19f0f295b..6e1e6ed975f 100644
--- a/.cursor/commands/add-trigger.md
+++ b/.cursor/commands/add-trigger.md
@@ -418,7 +418,6 @@ export const {service}PollingTrigger: TriggerConfig = {
subBlocks: [
{ id: 'triggerCredentials', type: 'oauth-input', title: 'Credentials', serviceId: '{service}', requiredScopes: [], required: true, mode: 'trigger', supportsCredentialSets: true },
// ... service-specific config fields (dropdowns, inputs, switches) ...
- { id: 'triggerSave', type: 'trigger-save', title: '', hideFromPreview: true, mode: 'trigger', triggerId: '{service}_poller' },
{ id: 'triggerInstructions', type: 'text', title: 'Setup Instructions', hideFromPreview: true, mode: 'trigger', defaultValue: '...' },
],
@@ -481,7 +480,6 @@ Add to `helm/sim/values.yaml` under the existing polling cron jobs:
- [ ] Handler implements `PollingProviderHandler` at `lib/webhooks/polling/{service}.ts`
- [ ] Trigger config has `polling: true` and defines subBlocks manually (no `buildTriggerSubBlocks`)
- [ ] Provider string matches across: trigger config, handler, `POLLING_PROVIDERS`, polling registry
-- [ ] `triggerSave` subBlock `triggerId` matches trigger config `id`
- [ ] First poll seeds state and emits nothing
- [ ] Added provider to `POLLING_PROVIDERS` in `triggers/constants.ts`
- [ ] Added handler to `POLLING_HANDLERS` in `lib/webhooks/polling/registry.ts`
diff --git a/apps/sim/blocks/blocks.test.ts b/apps/sim/blocks/blocks.test.ts
index 3421cb166ca..14468faaf7d 100644
--- a/apps/sim/blocks/blocks.test.ts
+++ b/apps/sim/blocks/blocks.test.ts
@@ -399,7 +399,6 @@ describe.concurrent('Blocks Module', () => {
'mcp-dynamic-args',
'input-format',
'response-format',
- 'trigger-save',
'file-upload',
'input-mapping',
'variables-input',
diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts
index 6b808a19a4a..d8aa7bb493d 100644
--- a/apps/sim/blocks/blocks/linear.ts
+++ b/apps/sim/blocks/blocks/linear.ts
@@ -2561,7 +2561,6 @@ export const LinearV2Block: BlockConfig = {
(sb) =>
!sb.id?.startsWith('webhookUrlDisplay') &&
!sb.id?.startsWith('webhookSecret') &&
- !sb.id?.startsWith('triggerSave') &&
!sb.id?.startsWith('triggerInstructions') &&
!sb.id?.startsWith('selectedTriggerId')
),
diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts
index 9cef227f35c..0d1b204e03f 100644
--- a/apps/sim/blocks/types.ts
+++ b/apps/sim/blocks/types.ts
@@ -153,10 +153,6 @@ export type SubBlockType =
| 'response-format' // Response structure format
| 'filter-builder' // Filter conditions builder
| 'sort-builder' // Sort conditions builder
- /**
- * @deprecated Legacy trigger save subblock type.
- */
- | 'trigger-save' // Trigger save button with validation
| 'file-upload' // File uploader
| 'input-mapping' // Map parent variables to child workflow input schema
| 'variables-input' // Variable assignments for updating workflow variables
@@ -414,8 +410,6 @@ export interface SubBlockConfig {
dependsOn?: string[] | { all?: string[]; any?: string[] }
// Copyable-text specific: Use webhook URL from webhook management hook
useWebhookUrl?: boolean
- // Trigger-save specific: The trigger ID for validation and saving
- triggerId?: string
// Dropdown/Combobox: Function to fetch options dynamically
// Works with both 'dropdown' (select-only) and 'combobox' (editable with expression support)
fetchOptions?: (blockId: string) => Promise>
diff --git a/apps/sim/tools/params.ts b/apps/sim/tools/params.ts
index e830d1acecf..991a8296957 100644
--- a/apps/sim/tools/params.ts
+++ b/apps/sim/tools/params.ts
@@ -869,7 +869,6 @@ const EXCLUDED_SUBBLOCK_TYPES = new Set([
'eval-input',
'webhook-config',
'schedule-info',
- 'trigger-save',
'input-format',
'response-format',
'mcp-server-selector',
diff --git a/apps/sim/triggers/airtable/webhook.ts b/apps/sim/triggers/airtable/webhook.ts
index a27f8ac3d2c..0c68cb83c08 100644
--- a/apps/sim/triggers/airtable/webhook.ts
+++ b/apps/sim/triggers/airtable/webhook.ts
@@ -47,14 +47,6 @@ export const airtableWebhookTrigger: TriggerConfig = {
defaultValue: false,
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'airtable_webhook',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/attio/utils.ts b/apps/sim/triggers/attio/utils.ts
index 9276f91ad46..9139a806188 100644
--- a/apps/sim/triggers/attio/utils.ts
+++ b/apps/sim/triggers/attio/utils.ts
@@ -56,13 +56,6 @@ export function buildAttioTriggerSubBlocks(triggerId: string): SubBlockConfig[]
required: true,
condition: { field: 'selectedTriggerId', value: triggerId },
},
- {
- id: 'triggerSave',
- title: 'Save',
- type: 'trigger-save',
- mode: 'trigger',
- condition: { field: 'selectedTriggerId', value: triggerId },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/calendly/invitee_canceled.ts b/apps/sim/triggers/calendly/invitee_canceled.ts
index d79c90dc4e9..9554f0aeb45 100644
--- a/apps/sim/triggers/calendly/invitee_canceled.ts
+++ b/apps/sim/triggers/calendly/invitee_canceled.ts
@@ -38,18 +38,6 @@ export const calendlyInviteeCanceledTrigger: TriggerConfig = {
value: 'calendly_invitee_canceled',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'calendly_invitee_canceled',
- condition: {
- field: 'selectedTriggerId',
- value: 'calendly_invitee_canceled',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/calendly/invitee_created.ts b/apps/sim/triggers/calendly/invitee_created.ts
index a0beb2fdfda..7c6c07937c6 100644
--- a/apps/sim/triggers/calendly/invitee_created.ts
+++ b/apps/sim/triggers/calendly/invitee_created.ts
@@ -47,18 +47,6 @@ export const calendlyInviteeCreatedTrigger: TriggerConfig = {
value: 'calendly_invitee_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'calendly_invitee_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'calendly_invitee_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/calendly/routing_form_submitted.ts b/apps/sim/triggers/calendly/routing_form_submitted.ts
index 9b0c3125be9..7ad1ecc5db7 100644
--- a/apps/sim/triggers/calendly/routing_form_submitted.ts
+++ b/apps/sim/triggers/calendly/routing_form_submitted.ts
@@ -38,18 +38,6 @@ export const calendlyRoutingFormSubmittedTrigger: TriggerConfig = {
value: 'calendly_routing_form_submitted',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'calendly_routing_form_submitted',
- condition: {
- field: 'selectedTriggerId',
- value: 'calendly_routing_form_submitted',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/calendly/webhook.ts b/apps/sim/triggers/calendly/webhook.ts
index 8506f34378b..2312b03d8c7 100644
--- a/apps/sim/triggers/calendly/webhook.ts
+++ b/apps/sim/triggers/calendly/webhook.ts
@@ -37,18 +37,6 @@ export const calendlyWebhookTrigger: TriggerConfig = {
value: 'calendly_webhook',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'calendly_webhook',
- condition: {
- field: 'selectedTriggerId',
- value: 'calendly_webhook',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/circleback/meeting_completed.ts b/apps/sim/triggers/circleback/meeting_completed.ts
index 9da936d5e84..6314409031e 100644
--- a/apps/sim/triggers/circleback/meeting_completed.ts
+++ b/apps/sim/triggers/circleback/meeting_completed.ts
@@ -39,18 +39,6 @@ export const circlebackMeetingCompletedTrigger: TriggerConfig = {
value: 'circleback_meeting_completed',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'circleback_meeting_completed',
- condition: {
- field: 'selectedTriggerId',
- value: 'circleback_meeting_completed',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/circleback/meeting_notes.ts b/apps/sim/triggers/circleback/meeting_notes.ts
index 5d814efcf39..add60b63425 100644
--- a/apps/sim/triggers/circleback/meeting_notes.ts
+++ b/apps/sim/triggers/circleback/meeting_notes.ts
@@ -39,18 +39,6 @@ export const circlebackMeetingNotesTrigger: TriggerConfig = {
value: 'circleback_meeting_notes',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'circleback_meeting_notes',
- condition: {
- field: 'selectedTriggerId',
- value: 'circleback_meeting_notes',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/circleback/webhook.ts b/apps/sim/triggers/circleback/webhook.ts
index f618deaf865..8fc8fe1af3d 100644
--- a/apps/sim/triggers/circleback/webhook.ts
+++ b/apps/sim/triggers/circleback/webhook.ts
@@ -48,18 +48,6 @@ export const circlebackWebhookTrigger: TriggerConfig = {
value: 'circleback_webhook',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'circleback_webhook',
- condition: {
- field: 'selectedTriggerId',
- value: 'circleback_webhook',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/constants.ts b/apps/sim/triggers/constants.ts
index 24f216835e5..800ee7e7094 100644
--- a/apps/sim/triggers/constants.ts
+++ b/apps/sim/triggers/constants.ts
@@ -11,7 +11,6 @@ export const SYSTEM_SUBBLOCK_IDS: string[] = [
'samplePayload', // Example payload display
'setupScript', // Setup script code (e.g., Apps Script)
'scheduleInfo', // Schedule status display (next run, last run)
- 'triggerSave', // UI-only save button — stores no config data
]
/**
diff --git a/apps/sim/triggers/fathom/new_meeting.ts b/apps/sim/triggers/fathom/new_meeting.ts
index de544febc93..4570061cc0d 100644
--- a/apps/sim/triggers/fathom/new_meeting.ts
+++ b/apps/sim/triggers/fathom/new_meeting.ts
@@ -91,18 +91,6 @@ export const fathomNewMeetingTrigger: TriggerConfig = {
value: 'fathom_new_meeting',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'fathom_new_meeting',
- condition: {
- field: 'selectedTriggerId',
- value: 'fathom_new_meeting',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/fathom/webhook.ts b/apps/sim/triggers/fathom/webhook.ts
index 96ddfc78774..4c6ded37ab1 100644
--- a/apps/sim/triggers/fathom/webhook.ts
+++ b/apps/sim/triggers/fathom/webhook.ts
@@ -91,18 +91,6 @@ export const fathomWebhookTrigger: TriggerConfig = {
value: 'fathom_webhook',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'fathom_webhook',
- condition: {
- field: 'selectedTriggerId',
- value: 'fathom_webhook',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/fireflies/transcription_complete.ts b/apps/sim/triggers/fireflies/transcription_complete.ts
index cca7ce97b90..37baf533892 100644
--- a/apps/sim/triggers/fireflies/transcription_complete.ts
+++ b/apps/sim/triggers/fireflies/transcription_complete.ts
@@ -30,14 +30,6 @@ export const firefliesTranscriptionCompleteTrigger: TriggerConfig = {
required: false,
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'fireflies_transcription_complete',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/generic/webhook.ts b/apps/sim/triggers/generic/webhook.ts
index 8c918cfaa81..db3fecc3be6 100644
--- a/apps/sim/triggers/generic/webhook.ts
+++ b/apps/sim/triggers/generic/webhook.ts
@@ -110,14 +110,6 @@ export const genericWebhookTrigger: TriggerConfig = {
'Define the expected JSON input schema for this webhook (optional). Use type "file[]" for file uploads.',
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'generic_webhook',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/github/issue_closed.ts b/apps/sim/triggers/github/issue_closed.ts
index aa22275a373..a25fd8abf3d 100644
--- a/apps/sim/triggers/github/issue_closed.ts
+++ b/apps/sim/triggers/github/issue_closed.ts
@@ -75,18 +75,6 @@ export const githubIssueClosedTrigger: TriggerConfig = {
value: 'github_issue_closed',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'github_issue_closed',
- condition: {
- field: 'selectedTriggerId',
- value: 'github_issue_closed',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/github/issue_comment.ts b/apps/sim/triggers/github/issue_comment.ts
index db40982e92b..6b94430c998 100644
--- a/apps/sim/triggers/github/issue_comment.ts
+++ b/apps/sim/triggers/github/issue_comment.ts
@@ -75,18 +75,6 @@ export const githubIssueCommentTrigger: TriggerConfig = {
value: 'github_issue_comment',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'github_issue_comment',
- condition: {
- field: 'selectedTriggerId',
- value: 'github_issue_comment',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/github/issue_opened.ts b/apps/sim/triggers/github/issue_opened.ts
index da4b2e1f2a6..c2e32ad1f53 100644
--- a/apps/sim/triggers/github/issue_opened.ts
+++ b/apps/sim/triggers/github/issue_opened.ts
@@ -96,18 +96,6 @@ export const githubIssueOpenedTrigger: TriggerConfig = {
value: 'github_issue_opened',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'github_issue_opened',
- condition: {
- field: 'selectedTriggerId',
- value: 'github_issue_opened',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/github/pr_closed.ts b/apps/sim/triggers/github/pr_closed.ts
index a654c0da431..1e52028a372 100644
--- a/apps/sim/triggers/github/pr_closed.ts
+++ b/apps/sim/triggers/github/pr_closed.ts
@@ -76,18 +76,6 @@ export const githubPRClosedTrigger: TriggerConfig = {
value: 'github_pr_closed',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'github_pr_closed',
- condition: {
- field: 'selectedTriggerId',
- value: 'github_pr_closed',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/github/pr_comment.ts b/apps/sim/triggers/github/pr_comment.ts
index 70b5f9a5c9f..bd61c373a37 100644
--- a/apps/sim/triggers/github/pr_comment.ts
+++ b/apps/sim/triggers/github/pr_comment.ts
@@ -75,18 +75,6 @@ export const githubPRCommentTrigger: TriggerConfig = {
value: 'github_pr_comment',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'github_pr_comment',
- condition: {
- field: 'selectedTriggerId',
- value: 'github_pr_comment',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/github/pr_merged.ts b/apps/sim/triggers/github/pr_merged.ts
index 24b2b8205c9..ad25e363a04 100644
--- a/apps/sim/triggers/github/pr_merged.ts
+++ b/apps/sim/triggers/github/pr_merged.ts
@@ -75,18 +75,6 @@ export const githubPRMergedTrigger: TriggerConfig = {
value: 'github_pr_merged',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'github_pr_merged',
- condition: {
- field: 'selectedTriggerId',
- value: 'github_pr_merged',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/github/pr_opened.ts b/apps/sim/triggers/github/pr_opened.ts
index 3288cc0c6fe..55f578f2cd5 100644
--- a/apps/sim/triggers/github/pr_opened.ts
+++ b/apps/sim/triggers/github/pr_opened.ts
@@ -75,18 +75,6 @@ export const githubPROpenedTrigger: TriggerConfig = {
value: 'github_pr_opened',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'github_pr_opened',
- condition: {
- field: 'selectedTriggerId',
- value: 'github_pr_opened',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/github/pr_reviewed.ts b/apps/sim/triggers/github/pr_reviewed.ts
index 8105f983f02..93074304c95 100644
--- a/apps/sim/triggers/github/pr_reviewed.ts
+++ b/apps/sim/triggers/github/pr_reviewed.ts
@@ -76,18 +76,6 @@ export const githubPRReviewedTrigger: TriggerConfig = {
value: 'github_pr_reviewed',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'github_pr_reviewed',
- condition: {
- field: 'selectedTriggerId',
- value: 'github_pr_reviewed',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/github/push.ts b/apps/sim/triggers/github/push.ts
index 36ce192e5d5..9dc16f6eaac 100644
--- a/apps/sim/triggers/github/push.ts
+++ b/apps/sim/triggers/github/push.ts
@@ -75,18 +75,6 @@ export const githubPushTrigger: TriggerConfig = {
value: 'github_push',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'github_push',
- condition: {
- field: 'selectedTriggerId',
- value: 'github_push',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/github/release_published.ts b/apps/sim/triggers/github/release_published.ts
index 7e8698d5a91..a0056bf1e68 100644
--- a/apps/sim/triggers/github/release_published.ts
+++ b/apps/sim/triggers/github/release_published.ts
@@ -75,18 +75,6 @@ export const githubReleasePublishedTrigger: TriggerConfig = {
value: 'github_release_published',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'github_release_published',
- condition: {
- field: 'selectedTriggerId',
- value: 'github_release_published',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/github/webhook.ts b/apps/sim/triggers/github/webhook.ts
index a73d61e262f..5b1b4c630ea 100644
--- a/apps/sim/triggers/github/webhook.ts
+++ b/apps/sim/triggers/github/webhook.ts
@@ -72,18 +72,6 @@ export const githubWebhookTrigger: TriggerConfig = {
value: 'github_webhook',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'github_webhook',
- condition: {
- field: 'selectedTriggerId',
- value: 'github_webhook',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/github/workflow_run.ts b/apps/sim/triggers/github/workflow_run.ts
index dc30c81b20a..138599280d4 100644
--- a/apps/sim/triggers/github/workflow_run.ts
+++ b/apps/sim/triggers/github/workflow_run.ts
@@ -76,18 +76,6 @@ export const githubWorkflowRunTrigger: TriggerConfig = {
value: 'github_workflow_run',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'github_workflow_run',
- condition: {
- field: 'selectedTriggerId',
- value: 'github_workflow_run',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/gmail/poller.ts b/apps/sim/triggers/gmail/poller.ts
index 772e254b93d..89d18c77e6a 100644
--- a/apps/sim/triggers/gmail/poller.ts
+++ b/apps/sim/triggers/gmail/poller.ts
@@ -152,14 +152,6 @@ Return ONLY the Gmail search query, no explanations or markdown.`,
required: false,
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'gmail_poller',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/google-calendar/poller.ts b/apps/sim/triggers/google-calendar/poller.ts
index 2b39cf1ab8d..977cd2df4cb 100644
--- a/apps/sim/triggers/google-calendar/poller.ts
+++ b/apps/sim/triggers/google-calendar/poller.ts
@@ -71,14 +71,6 @@ export const googleCalendarPollingTrigger: TriggerConfig = {
required: false,
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'google_calendar_poller',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/google-drive/poller.ts b/apps/sim/triggers/google-drive/poller.ts
index 6911643a6be..3f697a47b25 100644
--- a/apps/sim/triggers/google-drive/poller.ts
+++ b/apps/sim/triggers/google-drive/poller.ts
@@ -92,14 +92,6 @@ export const googleDrivePollingTrigger: TriggerConfig = {
required: false,
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'google_drive_poller',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/google-sheets/poller.ts b/apps/sim/triggers/google-sheets/poller.ts
index 0633b3fed64..ea5a51d1f51 100644
--- a/apps/sim/triggers/google-sheets/poller.ts
+++ b/apps/sim/triggers/google-sheets/poller.ts
@@ -98,14 +98,6 @@ export const googleSheetsPollingTrigger: TriggerConfig = {
required: false,
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'google_sheets_poller',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/googleforms/webhook.ts b/apps/sim/triggers/googleforms/webhook.ts
index 0f74fb1a946..e04665f2ce5 100644
--- a/apps/sim/triggers/googleforms/webhook.ts
+++ b/apps/sim/triggers/googleforms/webhook.ts
@@ -59,14 +59,6 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
defaultValue: true,
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'google_forms_webhook',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/grain/highlight_created.ts b/apps/sim/triggers/grain/highlight_created.ts
index 5f5bdb310c9..bce057c9a0e 100644
--- a/apps/sim/triggers/grain/highlight_created.ts
+++ b/apps/sim/triggers/grain/highlight_created.ts
@@ -38,18 +38,6 @@ export const grainHighlightCreatedTrigger: TriggerConfig = {
value: 'grain_highlight_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'grain_highlight_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'grain_highlight_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/grain/highlight_updated.ts b/apps/sim/triggers/grain/highlight_updated.ts
index 13c08b22e1c..f9e3a899687 100644
--- a/apps/sim/triggers/grain/highlight_updated.ts
+++ b/apps/sim/triggers/grain/highlight_updated.ts
@@ -38,18 +38,6 @@ export const grainHighlightUpdatedTrigger: TriggerConfig = {
value: 'grain_highlight_updated',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'grain_highlight_updated',
- condition: {
- field: 'selectedTriggerId',
- value: 'grain_highlight_updated',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/grain/item_added.ts b/apps/sim/triggers/grain/item_added.ts
index 1bca0d1b782..76bd4ba872c 100644
--- a/apps/sim/triggers/grain/item_added.ts
+++ b/apps/sim/triggers/grain/item_added.ts
@@ -39,18 +39,6 @@ export const grainItemAddedTrigger: TriggerConfig = {
value: 'grain_item_added',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'grain_item_added',
- condition: {
- field: 'selectedTriggerId',
- value: 'grain_item_added',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/grain/item_updated.ts b/apps/sim/triggers/grain/item_updated.ts
index ca6b7b11b13..b06706ad696 100644
--- a/apps/sim/triggers/grain/item_updated.ts
+++ b/apps/sim/triggers/grain/item_updated.ts
@@ -39,18 +39,6 @@ export const grainItemUpdatedTrigger: TriggerConfig = {
value: 'grain_item_updated',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'grain_item_updated',
- condition: {
- field: 'selectedTriggerId',
- value: 'grain_item_updated',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/grain/recording_created.ts b/apps/sim/triggers/grain/recording_created.ts
index c2af9a70866..83a43b85d53 100644
--- a/apps/sim/triggers/grain/recording_created.ts
+++ b/apps/sim/triggers/grain/recording_created.ts
@@ -38,18 +38,6 @@ export const grainRecordingCreatedTrigger: TriggerConfig = {
value: 'grain_recording_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'grain_recording_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'grain_recording_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/grain/recording_updated.ts b/apps/sim/triggers/grain/recording_updated.ts
index 22c74fc5862..4b402418a87 100644
--- a/apps/sim/triggers/grain/recording_updated.ts
+++ b/apps/sim/triggers/grain/recording_updated.ts
@@ -38,18 +38,6 @@ export const grainRecordingUpdatedTrigger: TriggerConfig = {
value: 'grain_recording_updated',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'grain_recording_updated',
- condition: {
- field: 'selectedTriggerId',
- value: 'grain_recording_updated',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/grain/story_created.ts b/apps/sim/triggers/grain/story_created.ts
index f4a1dc4c111..d50d3415059 100644
--- a/apps/sim/triggers/grain/story_created.ts
+++ b/apps/sim/triggers/grain/story_created.ts
@@ -38,18 +38,6 @@ export const grainStoryCreatedTrigger: TriggerConfig = {
value: 'grain_story_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'grain_story_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'grain_story_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/grain/webhook.ts b/apps/sim/triggers/grain/webhook.ts
index 5e858ca67cb..25ee70c0b12 100644
--- a/apps/sim/triggers/grain/webhook.ts
+++ b/apps/sim/triggers/grain/webhook.ts
@@ -39,18 +39,6 @@ export const grainWebhookTrigger: TriggerConfig = {
value: 'grain_webhook',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'grain_webhook',
- condition: {
- field: 'selectedTriggerId',
- value: 'grain_webhook',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/company_created.ts b/apps/sim/triggers/hubspot/company_created.ts
index 3a26e1ac663..54458cfc220 100644
--- a/apps/sim/triggers/hubspot/company_created.ts
+++ b/apps/sim/triggers/hubspot/company_created.ts
@@ -93,17 +93,6 @@ export const hubspotCompanyCreatedTrigger: TriggerConfig = {
value: 'hubspot_company_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_company_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_company_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/company_deleted.ts b/apps/sim/triggers/hubspot/company_deleted.ts
index 654cb303951..07bc0026bc4 100644
--- a/apps/sim/triggers/hubspot/company_deleted.ts
+++ b/apps/sim/triggers/hubspot/company_deleted.ts
@@ -93,17 +93,6 @@ export const hubspotCompanyDeletedTrigger: TriggerConfig = {
value: 'hubspot_company_deleted',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_company_deleted',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_company_deleted',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/company_merged.ts b/apps/sim/triggers/hubspot/company_merged.ts
index 4d64cf7ad3e..a524bebc2f1 100644
--- a/apps/sim/triggers/hubspot/company_merged.ts
+++ b/apps/sim/triggers/hubspot/company_merged.ts
@@ -93,17 +93,6 @@ export const hubspotCompanyMergedTrigger: TriggerConfig = {
value: 'hubspot_company_merged',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_company_merged',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_company_merged',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/company_property_changed.ts b/apps/sim/triggers/hubspot/company_property_changed.ts
index c34e62b656e..fb6d6e156c6 100644
--- a/apps/sim/triggers/hubspot/company_property_changed.ts
+++ b/apps/sim/triggers/hubspot/company_property_changed.ts
@@ -107,17 +107,6 @@ export const hubspotCompanyPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_company_property_changed',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_company_property_changed',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_company_property_changed',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/company_restored.ts b/apps/sim/triggers/hubspot/company_restored.ts
index a30528c6954..e7b3b9a88d4 100644
--- a/apps/sim/triggers/hubspot/company_restored.ts
+++ b/apps/sim/triggers/hubspot/company_restored.ts
@@ -93,17 +93,6 @@ export const hubspotCompanyRestoredTrigger: TriggerConfig = {
value: 'hubspot_company_restored',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_company_restored',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_company_restored',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/contact_created.ts b/apps/sim/triggers/hubspot/contact_created.ts
index 0984e733399..854850c185d 100644
--- a/apps/sim/triggers/hubspot/contact_created.ts
+++ b/apps/sim/triggers/hubspot/contact_created.ts
@@ -93,17 +93,6 @@ export const hubspotContactCreatedTrigger: TriggerConfig = {
value: 'hubspot_contact_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_contact_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_contact_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/contact_deleted.ts b/apps/sim/triggers/hubspot/contact_deleted.ts
index 767ddb18616..fb0aa85f7c8 100644
--- a/apps/sim/triggers/hubspot/contact_deleted.ts
+++ b/apps/sim/triggers/hubspot/contact_deleted.ts
@@ -93,17 +93,6 @@ export const hubspotContactDeletedTrigger: TriggerConfig = {
value: 'hubspot_contact_deleted',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_contact_deleted',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_contact_deleted',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/contact_merged.ts b/apps/sim/triggers/hubspot/contact_merged.ts
index 435399e7706..125121f7442 100644
--- a/apps/sim/triggers/hubspot/contact_merged.ts
+++ b/apps/sim/triggers/hubspot/contact_merged.ts
@@ -93,17 +93,6 @@ export const hubspotContactMergedTrigger: TriggerConfig = {
value: 'hubspot_contact_merged',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_contact_merged',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_contact_merged',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/contact_privacy_deleted.ts b/apps/sim/triggers/hubspot/contact_privacy_deleted.ts
index 9da858923ff..10fd627e200 100644
--- a/apps/sim/triggers/hubspot/contact_privacy_deleted.ts
+++ b/apps/sim/triggers/hubspot/contact_privacy_deleted.ts
@@ -94,17 +94,6 @@ export const hubspotContactPrivacyDeletedTrigger: TriggerConfig = {
value: 'hubspot_contact_privacy_deleted',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_contact_privacy_deleted',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_contact_privacy_deleted',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/contact_property_changed.ts b/apps/sim/triggers/hubspot/contact_property_changed.ts
index 27dfb7a93fb..f68ef966fbc 100644
--- a/apps/sim/triggers/hubspot/contact_property_changed.ts
+++ b/apps/sim/triggers/hubspot/contact_property_changed.ts
@@ -107,17 +107,6 @@ export const hubspotContactPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_contact_property_changed',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_contact_property_changed',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_contact_property_changed',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/contact_restored.ts b/apps/sim/triggers/hubspot/contact_restored.ts
index e3ad2113da6..e8c02123aa4 100644
--- a/apps/sim/triggers/hubspot/contact_restored.ts
+++ b/apps/sim/triggers/hubspot/contact_restored.ts
@@ -93,17 +93,6 @@ export const hubspotContactRestoredTrigger: TriggerConfig = {
value: 'hubspot_contact_restored',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_contact_restored',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_contact_restored',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/conversation_creation.ts b/apps/sim/triggers/hubspot/conversation_creation.ts
index 1d1b30e6fc5..6cc12b3965c 100644
--- a/apps/sim/triggers/hubspot/conversation_creation.ts
+++ b/apps/sim/triggers/hubspot/conversation_creation.ts
@@ -93,17 +93,6 @@ export const hubspotConversationCreationTrigger: TriggerConfig = {
value: 'hubspot_conversation_creation',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_conversation_creation',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_conversation_creation',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/conversation_deletion.ts b/apps/sim/triggers/hubspot/conversation_deletion.ts
index 8299b49f7b7..1b0c7e2faaf 100644
--- a/apps/sim/triggers/hubspot/conversation_deletion.ts
+++ b/apps/sim/triggers/hubspot/conversation_deletion.ts
@@ -93,17 +93,6 @@ export const hubspotConversationDeletionTrigger: TriggerConfig = {
value: 'hubspot_conversation_deletion',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_conversation_deletion',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_conversation_deletion',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/conversation_new_message.ts b/apps/sim/triggers/hubspot/conversation_new_message.ts
index 0f9007ea9fa..8611f0845c2 100644
--- a/apps/sim/triggers/hubspot/conversation_new_message.ts
+++ b/apps/sim/triggers/hubspot/conversation_new_message.ts
@@ -93,17 +93,6 @@ export const hubspotConversationNewMessageTrigger: TriggerConfig = {
value: 'hubspot_conversation_new_message',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_conversation_new_message',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_conversation_new_message',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/conversation_privacy_deletion.ts b/apps/sim/triggers/hubspot/conversation_privacy_deletion.ts
index bc269c4b44d..7d170c7b84a 100644
--- a/apps/sim/triggers/hubspot/conversation_privacy_deletion.ts
+++ b/apps/sim/triggers/hubspot/conversation_privacy_deletion.ts
@@ -94,17 +94,6 @@ export const hubspotConversationPrivacyDeletionTrigger: TriggerConfig = {
value: 'hubspot_conversation_privacy_deletion',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_conversation_privacy_deletion',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_conversation_privacy_deletion',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/conversation_property_changed.ts b/apps/sim/triggers/hubspot/conversation_property_changed.ts
index efe19e07e0e..8d3fabd66db 100644
--- a/apps/sim/triggers/hubspot/conversation_property_changed.ts
+++ b/apps/sim/triggers/hubspot/conversation_property_changed.ts
@@ -107,17 +107,6 @@ export const hubspotConversationPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_conversation_property_changed',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_conversation_property_changed',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_conversation_property_changed',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/deal_created.ts b/apps/sim/triggers/hubspot/deal_created.ts
index a4923de69a5..f1e6838577a 100644
--- a/apps/sim/triggers/hubspot/deal_created.ts
+++ b/apps/sim/triggers/hubspot/deal_created.ts
@@ -93,17 +93,6 @@ export const hubspotDealCreatedTrigger: TriggerConfig = {
value: 'hubspot_deal_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_deal_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_deal_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/deal_deleted.ts b/apps/sim/triggers/hubspot/deal_deleted.ts
index b53ab112ceb..92765a970ae 100644
--- a/apps/sim/triggers/hubspot/deal_deleted.ts
+++ b/apps/sim/triggers/hubspot/deal_deleted.ts
@@ -93,17 +93,6 @@ export const hubspotDealDeletedTrigger: TriggerConfig = {
value: 'hubspot_deal_deleted',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_deal_deleted',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_deal_deleted',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/deal_merged.ts b/apps/sim/triggers/hubspot/deal_merged.ts
index e6d875af02d..06ce88fc4f4 100644
--- a/apps/sim/triggers/hubspot/deal_merged.ts
+++ b/apps/sim/triggers/hubspot/deal_merged.ts
@@ -93,17 +93,6 @@ export const hubspotDealMergedTrigger: TriggerConfig = {
value: 'hubspot_deal_merged',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_deal_merged',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_deal_merged',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/deal_property_changed.ts b/apps/sim/triggers/hubspot/deal_property_changed.ts
index a49bbeb26d8..f4e58b3147a 100644
--- a/apps/sim/triggers/hubspot/deal_property_changed.ts
+++ b/apps/sim/triggers/hubspot/deal_property_changed.ts
@@ -107,17 +107,6 @@ export const hubspotDealPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_deal_property_changed',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_deal_property_changed',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_deal_property_changed',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/deal_restored.ts b/apps/sim/triggers/hubspot/deal_restored.ts
index ba3eb28b8c4..b511c02ccb5 100644
--- a/apps/sim/triggers/hubspot/deal_restored.ts
+++ b/apps/sim/triggers/hubspot/deal_restored.ts
@@ -93,17 +93,6 @@ export const hubspotDealRestoredTrigger: TriggerConfig = {
value: 'hubspot_deal_restored',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_deal_restored',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_deal_restored',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/ticket_created.ts b/apps/sim/triggers/hubspot/ticket_created.ts
index 1cff8fb8a41..70201fcd2a5 100644
--- a/apps/sim/triggers/hubspot/ticket_created.ts
+++ b/apps/sim/triggers/hubspot/ticket_created.ts
@@ -93,17 +93,6 @@ export const hubspotTicketCreatedTrigger: TriggerConfig = {
value: 'hubspot_ticket_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_ticket_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_ticket_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/ticket_deleted.ts b/apps/sim/triggers/hubspot/ticket_deleted.ts
index 28ef9748b78..d1152c32e61 100644
--- a/apps/sim/triggers/hubspot/ticket_deleted.ts
+++ b/apps/sim/triggers/hubspot/ticket_deleted.ts
@@ -93,17 +93,6 @@ export const hubspotTicketDeletedTrigger: TriggerConfig = {
value: 'hubspot_ticket_deleted',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_ticket_deleted',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_ticket_deleted',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/ticket_merged.ts b/apps/sim/triggers/hubspot/ticket_merged.ts
index ce860ab1465..737f3c81ef6 100644
--- a/apps/sim/triggers/hubspot/ticket_merged.ts
+++ b/apps/sim/triggers/hubspot/ticket_merged.ts
@@ -93,17 +93,6 @@ export const hubspotTicketMergedTrigger: TriggerConfig = {
value: 'hubspot_ticket_merged',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_ticket_merged',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_ticket_merged',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/ticket_property_changed.ts b/apps/sim/triggers/hubspot/ticket_property_changed.ts
index f7dbcf9acf4..104a77b5524 100644
--- a/apps/sim/triggers/hubspot/ticket_property_changed.ts
+++ b/apps/sim/triggers/hubspot/ticket_property_changed.ts
@@ -107,17 +107,6 @@ export const hubspotTicketPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_ticket_property_changed',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_ticket_property_changed',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_ticket_property_changed',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/ticket_restored.ts b/apps/sim/triggers/hubspot/ticket_restored.ts
index 028082658b4..18e8cded6bb 100644
--- a/apps/sim/triggers/hubspot/ticket_restored.ts
+++ b/apps/sim/triggers/hubspot/ticket_restored.ts
@@ -93,17 +93,6 @@ export const hubspotTicketRestoredTrigger: TriggerConfig = {
value: 'hubspot_ticket_restored',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_ticket_restored',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_ticket_restored',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/hubspot/webhook.ts b/apps/sim/triggers/hubspot/webhook.ts
index addcb92b923..aeb303ff051 100644
--- a/apps/sim/triggers/hubspot/webhook.ts
+++ b/apps/sim/triggers/hubspot/webhook.ts
@@ -93,17 +93,6 @@ export const hubspotWebhookTrigger: TriggerConfig = {
value: 'hubspot_webhook',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- mode: 'trigger',
- triggerId: 'hubspot_webhook',
- condition: {
- field: 'selectedTriggerId',
- value: 'hubspot_webhook',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/imap/poller.ts b/apps/sim/triggers/imap/poller.ts
index ca4cfd18ab5..69719e29d36 100644
--- a/apps/sim/triggers/imap/poller.ts
+++ b/apps/sim/triggers/imap/poller.ts
@@ -192,14 +192,6 @@ Return ONLY valid JSON, no explanations or markdown.`,
mode: 'trigger',
},
// Instructions
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'imap_poller',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/index.ts b/apps/sim/triggers/index.ts
index f99511c7db7..f5f6f5949ed 100644
--- a/apps/sim/triggers/index.ts
+++ b/apps/sim/triggers/index.ts
@@ -73,9 +73,11 @@ export function getTrigger(triggerId: string): TriggerConfig {
throw new Error(`Trigger not found: ${triggerId}`)
}
- // Clone and filter out deprecated trigger-save subblocks
+ // Filter out deprecated trigger-save subblocks from legacy stored data
const subBlocks = trigger.subBlocks
- .filter((subBlock) => subBlock.id !== 'triggerSave' && subBlock.type !== 'trigger-save')
+ .filter(
+ (subBlock) => subBlock.id !== 'triggerSave' && (subBlock.type as string) !== 'trigger-save'
+ )
.map((subBlock) => namespaceSubBlockId(subBlock, triggerId))
const clonedTrigger = { ...trigger, subBlocks }
@@ -154,7 +156,7 @@ export interface BuildTriggerSubBlocksOptions {
/**
* Generic builder for trigger subBlocks.
- * Creates a consistent structure: [dropdown?] -> webhookUrl -> extraFields -> save -> instructions
+ * Creates a consistent structure: [dropdown?] -> webhookUrl -> extraFields -> instructions
*
* Usage:
* - Primary trigger: `buildTriggerSubBlocks({ ...options, includeDropdown: true })`
diff --git a/apps/sim/triggers/jira/issue_commented.ts b/apps/sim/triggers/jira/issue_commented.ts
index 348a0c889cf..ad84ade7b29 100644
--- a/apps/sim/triggers/jira/issue_commented.ts
+++ b/apps/sim/triggers/jira/issue_commented.ts
@@ -56,18 +56,6 @@ export const jiraIssueCommentedTrigger: TriggerConfig = {
value: 'jira_issue_commented',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'jira_issue_commented',
- condition: {
- field: 'selectedTriggerId',
- value: 'jira_issue_commented',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/jira/issue_created.ts b/apps/sim/triggers/jira/issue_created.ts
index df9a5f8f83d..ed9dd77cb2a 100644
--- a/apps/sim/triggers/jira/issue_created.ts
+++ b/apps/sim/triggers/jira/issue_created.ts
@@ -65,18 +65,6 @@ export const jiraIssueCreatedTrigger: TriggerConfig = {
value: 'jira_issue_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'jira_issue_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'jira_issue_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/jira/issue_deleted.ts b/apps/sim/triggers/jira/issue_deleted.ts
index 190c23739eb..21ee8ce5c89 100644
--- a/apps/sim/triggers/jira/issue_deleted.ts
+++ b/apps/sim/triggers/jira/issue_deleted.ts
@@ -56,18 +56,6 @@ export const jiraIssueDeletedTrigger: TriggerConfig = {
value: 'jira_issue_deleted',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'jira_issue_deleted',
- condition: {
- field: 'selectedTriggerId',
- value: 'jira_issue_deleted',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/jira/issue_updated.ts b/apps/sim/triggers/jira/issue_updated.ts
index 52189c17931..3c70ce424c0 100644
--- a/apps/sim/triggers/jira/issue_updated.ts
+++ b/apps/sim/triggers/jira/issue_updated.ts
@@ -70,18 +70,6 @@ export const jiraIssueUpdatedTrigger: TriggerConfig = {
value: 'jira_issue_updated',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'jira_issue_updated',
- condition: {
- field: 'selectedTriggerId',
- value: 'jira_issue_updated',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/jira/webhook.ts b/apps/sim/triggers/jira/webhook.ts
index c44a5aea3b4..57c94751935 100644
--- a/apps/sim/triggers/jira/webhook.ts
+++ b/apps/sim/triggers/jira/webhook.ts
@@ -43,18 +43,6 @@ export const jiraWebhookTrigger: TriggerConfig = {
value: 'jira_webhook',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'jira_webhook',
- condition: {
- field: 'selectedTriggerId',
- value: 'jira_webhook',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/jira/worklog_created.ts b/apps/sim/triggers/jira/worklog_created.ts
index f2603deb355..94f5e76ff82 100644
--- a/apps/sim/triggers/jira/worklog_created.ts
+++ b/apps/sim/triggers/jira/worklog_created.ts
@@ -56,18 +56,6 @@ export const jiraWorklogCreatedTrigger: TriggerConfig = {
value: 'jira_worklog_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'jira_worklog_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'jira_worklog_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/comment_created.ts b/apps/sim/triggers/linear/comment_created.ts
index f4d9e779011..df94c5487c7 100644
--- a/apps/sim/triggers/linear/comment_created.ts
+++ b/apps/sim/triggers/linear/comment_created.ts
@@ -43,18 +43,6 @@ export const linearCommentCreatedTrigger: TriggerConfig = {
value: 'linear_comment_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_comment_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_comment_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/comment_updated.ts b/apps/sim/triggers/linear/comment_updated.ts
index 6bd1761beba..5d7807ed035 100644
--- a/apps/sim/triggers/linear/comment_updated.ts
+++ b/apps/sim/triggers/linear/comment_updated.ts
@@ -43,18 +43,6 @@ export const linearCommentUpdatedTrigger: TriggerConfig = {
value: 'linear_comment_updated',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_comment_updated',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_comment_updated',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/customer_request_created.ts b/apps/sim/triggers/linear/customer_request_created.ts
index b8c17da527d..fdf9648f7be 100644
--- a/apps/sim/triggers/linear/customer_request_created.ts
+++ b/apps/sim/triggers/linear/customer_request_created.ts
@@ -43,18 +43,6 @@ export const linearCustomerRequestCreatedTrigger: TriggerConfig = {
value: 'linear_customer_request_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_customer_request_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_customer_request_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/customer_request_updated.ts b/apps/sim/triggers/linear/customer_request_updated.ts
index a76b8c22cab..20b669d1ba9 100644
--- a/apps/sim/triggers/linear/customer_request_updated.ts
+++ b/apps/sim/triggers/linear/customer_request_updated.ts
@@ -43,18 +43,6 @@ export const linearCustomerRequestUpdatedTrigger: TriggerConfig = {
value: 'linear_customer_request_updated',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_customer_request_updated',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_customer_request_updated',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/cycle_created.ts b/apps/sim/triggers/linear/cycle_created.ts
index 3238dce74fe..fe97896b369 100644
--- a/apps/sim/triggers/linear/cycle_created.ts
+++ b/apps/sim/triggers/linear/cycle_created.ts
@@ -43,18 +43,6 @@ export const linearCycleCreatedTrigger: TriggerConfig = {
value: 'linear_cycle_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_cycle_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_cycle_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/cycle_updated.ts b/apps/sim/triggers/linear/cycle_updated.ts
index fc996c3a8e8..387c0769292 100644
--- a/apps/sim/triggers/linear/cycle_updated.ts
+++ b/apps/sim/triggers/linear/cycle_updated.ts
@@ -43,18 +43,6 @@ export const linearCycleUpdatedTrigger: TriggerConfig = {
value: 'linear_cycle_updated',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_cycle_updated',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_cycle_updated',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/issue_created.ts b/apps/sim/triggers/linear/issue_created.ts
index 4a95974a1e7..cfcb91749e5 100644
--- a/apps/sim/triggers/linear/issue_created.ts
+++ b/apps/sim/triggers/linear/issue_created.ts
@@ -53,18 +53,6 @@ export const linearIssueCreatedTrigger: TriggerConfig = {
value: 'linear_issue_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_issue_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_issue_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/issue_removed.ts b/apps/sim/triggers/linear/issue_removed.ts
index ca1431ecaeb..bc10799b424 100644
--- a/apps/sim/triggers/linear/issue_removed.ts
+++ b/apps/sim/triggers/linear/issue_removed.ts
@@ -43,18 +43,6 @@ export const linearIssueRemovedTrigger: TriggerConfig = {
value: 'linear_issue_removed',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_issue_removed',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_issue_removed',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/issue_updated.ts b/apps/sim/triggers/linear/issue_updated.ts
index 2893331a184..a3cca23eec1 100644
--- a/apps/sim/triggers/linear/issue_updated.ts
+++ b/apps/sim/triggers/linear/issue_updated.ts
@@ -43,18 +43,6 @@ export const linearIssueUpdatedTrigger: TriggerConfig = {
value: 'linear_issue_updated',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_issue_updated',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_issue_updated',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/label_created.ts b/apps/sim/triggers/linear/label_created.ts
index 369825c83aa..3721ec4515d 100644
--- a/apps/sim/triggers/linear/label_created.ts
+++ b/apps/sim/triggers/linear/label_created.ts
@@ -43,18 +43,6 @@ export const linearLabelCreatedTrigger: TriggerConfig = {
value: 'linear_label_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_label_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_label_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/label_updated.ts b/apps/sim/triggers/linear/label_updated.ts
index 9009165bf5b..9b047a3c550 100644
--- a/apps/sim/triggers/linear/label_updated.ts
+++ b/apps/sim/triggers/linear/label_updated.ts
@@ -43,18 +43,6 @@ export const linearLabelUpdatedTrigger: TriggerConfig = {
value: 'linear_label_updated',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_label_updated',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_label_updated',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/project_created.ts b/apps/sim/triggers/linear/project_created.ts
index 6758466c701..99e9459f2bf 100644
--- a/apps/sim/triggers/linear/project_created.ts
+++ b/apps/sim/triggers/linear/project_created.ts
@@ -43,18 +43,6 @@ export const linearProjectCreatedTrigger: TriggerConfig = {
value: 'linear_project_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_project_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_project_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/project_update_created.ts b/apps/sim/triggers/linear/project_update_created.ts
index 83321dbf80c..cf439f6b53a 100644
--- a/apps/sim/triggers/linear/project_update_created.ts
+++ b/apps/sim/triggers/linear/project_update_created.ts
@@ -43,18 +43,6 @@ export const linearProjectUpdateCreatedTrigger: TriggerConfig = {
value: 'linear_project_update_created',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_project_update_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_project_update_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/project_updated.ts b/apps/sim/triggers/linear/project_updated.ts
index e79eb4cd58a..592baf90897 100644
--- a/apps/sim/triggers/linear/project_updated.ts
+++ b/apps/sim/triggers/linear/project_updated.ts
@@ -43,18 +43,6 @@ export const linearProjectUpdatedTrigger: TriggerConfig = {
value: 'linear_project_updated',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_project_updated',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_project_updated',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/utils.ts b/apps/sim/triggers/linear/utils.ts
index de9f9122275..a1bd42138b7 100644
--- a/apps/sim/triggers/linear/utils.ts
+++ b/apps/sim/triggers/linear/utils.ts
@@ -126,7 +126,7 @@ export const linearV2TriggerOptions = [
* Builds the complete subBlocks array for a v2 Linear trigger.
* Webhooks are managed via API, so no webhook URL is displayed.
*
- * Structure: [dropdown?] -> apiKey -> triggerSave -> instructions
+ * Structure: [dropdown?] -> apiKey -> instructions
*/
export function buildLinearV2SubBlocks(options: {
triggerId: string
@@ -170,16 +170,6 @@ export function buildLinearV2SubBlocks(options: {
condition: { field: 'selectedTriggerId', value: triggerId },
})
- blocks.push({
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId,
- condition: { field: 'selectedTriggerId', value: triggerId },
- })
-
blocks.push({
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/linear/webhook.ts b/apps/sim/triggers/linear/webhook.ts
index 83ad6d6a25d..cd3899b9bee 100644
--- a/apps/sim/triggers/linear/webhook.ts
+++ b/apps/sim/triggers/linear/webhook.ts
@@ -44,18 +44,6 @@ export const linearWebhookTrigger: TriggerConfig = {
value: 'linear_webhook',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'linear_webhook',
- condition: {
- field: 'selectedTriggerId',
- value: 'linear_webhook',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/microsoftteams/chat_webhook.ts b/apps/sim/triggers/microsoftteams/chat_webhook.ts
index 9ef0b439071..d577cbe84ba 100644
--- a/apps/sim/triggers/microsoftteams/chat_webhook.ts
+++ b/apps/sim/triggers/microsoftteams/chat_webhook.ts
@@ -72,18 +72,6 @@ export const microsoftTeamsChatSubscriptionTrigger: TriggerConfig = {
value: 'microsoftteams_chat_subscription',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'microsoftteams_chat_subscription',
- condition: {
- field: 'selectedTriggerId',
- value: 'microsoftteams_chat_subscription',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/microsoftteams/webhook.ts b/apps/sim/triggers/microsoftteams/webhook.ts
index 1f17a771946..15224ff3b00 100644
--- a/apps/sim/triggers/microsoftteams/webhook.ts
+++ b/apps/sim/triggers/microsoftteams/webhook.ts
@@ -51,18 +51,6 @@ export const microsoftTeamsWebhookTrigger: TriggerConfig = {
value: 'microsoftteams_webhook',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'microsoftteams_webhook',
- condition: {
- field: 'selectedTriggerId',
- value: 'microsoftteams_webhook',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/outlook/poller.ts b/apps/sim/triggers/outlook/poller.ts
index a4af3961576..8c3930bd279 100644
--- a/apps/sim/triggers/outlook/poller.ts
+++ b/apps/sim/triggers/outlook/poller.ts
@@ -111,14 +111,6 @@ export const outlookPollingTrigger: TriggerConfig = {
required: false,
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'outlook_poller',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/rss/poller.ts b/apps/sim/triggers/rss/poller.ts
index 0877ee06356..81a87236ee7 100644
--- a/apps/sim/triggers/rss/poller.ts
+++ b/apps/sim/triggers/rss/poller.ts
@@ -20,14 +20,6 @@ export const rssPollingTrigger: TriggerConfig = {
required: true,
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'rss_poller',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/slack/webhook.ts b/apps/sim/triggers/slack/webhook.ts
index 2fa8966ae63..72037b6bd82 100644
--- a/apps/sim/triggers/slack/webhook.ts
+++ b/apps/sim/triggers/slack/webhook.ts
@@ -51,14 +51,6 @@ export const slackWebhookTrigger: TriggerConfig = {
required: false,
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'slack_webhook',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/stripe/webhook.ts b/apps/sim/triggers/stripe/webhook.ts
index 4000870db29..e087b619346 100644
--- a/apps/sim/triggers/stripe/webhook.ts
+++ b/apps/sim/triggers/stripe/webhook.ts
@@ -165,14 +165,6 @@ export const stripeWebhookTrigger: TriggerConfig = {
password: true,
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'stripe_webhook',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/telegram/webhook.ts b/apps/sim/triggers/telegram/webhook.ts
index 6039370a36a..f9025f8914e 100644
--- a/apps/sim/triggers/telegram/webhook.ts
+++ b/apps/sim/triggers/telegram/webhook.ts
@@ -30,14 +30,6 @@ export const telegramWebhookTrigger: TriggerConfig = {
required: true,
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'telegram_webhook',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/twilio_voice/webhook.ts b/apps/sim/triggers/twilio_voice/webhook.ts
index 5c4cf4de25d..d447f54adbd 100644
--- a/apps/sim/triggers/twilio_voice/webhook.ts
+++ b/apps/sim/triggers/twilio_voice/webhook.ts
@@ -108,14 +108,6 @@ Return ONLY the TwiML with square brackets - no explanations, no markdown, no ex
placeholder: 'Describe what should happen when a call comes in...',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'twilio_voice_webhook',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/typeform/webhook.ts b/apps/sim/triggers/typeform/webhook.ts
index 6ff40a4a28d..53bec744e3e 100644
--- a/apps/sim/triggers/typeform/webhook.ts
+++ b/apps/sim/triggers/typeform/webhook.ts
@@ -61,14 +61,6 @@ export const typeformWebhookTrigger: TriggerConfig = {
defaultValue: false,
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'typeform_webhook',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/webflow/collection_item_changed.ts b/apps/sim/triggers/webflow/collection_item_changed.ts
index 976b58c8f53..7d9e697ba8d 100644
--- a/apps/sim/triggers/webflow/collection_item_changed.ts
+++ b/apps/sim/triggers/webflow/collection_item_changed.ts
@@ -167,18 +167,6 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
},
dependsOn: ['triggerCredentials', 'triggerSiteId'],
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'webflow_collection_item_changed',
- condition: {
- field: 'selectedTriggerId',
- value: 'webflow_collection_item_changed',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/webflow/collection_item_created.ts b/apps/sim/triggers/webflow/collection_item_created.ts
index 4494b08108a..fc04ab70940 100644
--- a/apps/sim/triggers/webflow/collection_item_created.ts
+++ b/apps/sim/triggers/webflow/collection_item_created.ts
@@ -181,18 +181,6 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
},
dependsOn: ['triggerCredentials', 'triggerSiteId'],
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'webflow_collection_item_created',
- condition: {
- field: 'selectedTriggerId',
- value: 'webflow_collection_item_created',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/webflow/collection_item_deleted.ts b/apps/sim/triggers/webflow/collection_item_deleted.ts
index e4e3d1f033f..a0bfdd85b1b 100644
--- a/apps/sim/triggers/webflow/collection_item_deleted.ts
+++ b/apps/sim/triggers/webflow/collection_item_deleted.ts
@@ -167,18 +167,6 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
},
dependsOn: ['triggerCredentials', 'triggerSiteId'],
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'webflow_collection_item_deleted',
- condition: {
- field: 'selectedTriggerId',
- value: 'webflow_collection_item_deleted',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/webflow/form_submission.ts b/apps/sim/triggers/webflow/form_submission.ts
index 3ea3494696c..47f2f8cf8a0 100644
--- a/apps/sim/triggers/webflow/form_submission.ts
+++ b/apps/sim/triggers/webflow/form_submission.ts
@@ -109,18 +109,6 @@ export const webflowFormSubmissionTrigger: TriggerConfig = {
value: 'webflow_form_submission',
},
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'webflow_form_submission',
- condition: {
- field: 'selectedTriggerId',
- value: 'webflow_form_submission',
- },
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/apps/sim/triggers/whatsapp/webhook.ts b/apps/sim/triggers/whatsapp/webhook.ts
index 67f04c955f8..8ae735d33fd 100644
--- a/apps/sim/triggers/whatsapp/webhook.ts
+++ b/apps/sim/triggers/whatsapp/webhook.ts
@@ -31,14 +31,6 @@ export const whatsappWebhookTrigger: TriggerConfig = {
required: true,
mode: 'trigger',
},
- {
- id: 'triggerSave',
- title: '',
- type: 'trigger-save',
- hideFromPreview: true,
- mode: 'trigger',
- triggerId: 'whatsapp_webhook',
- },
{
id: 'triggerInstructions',
title: 'Setup Instructions',
diff --git a/bun.lock b/bun.lock
index f8bde9a6cf3..e05bc532f5e 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
+ "configVersion": 0,
"workspaces": {
"": {
"name": "simstudio",
From 74d0a475250af509060f8570cb867e14006ea565 Mon Sep 17 00:00:00 2001
From: Waleed
Date: Sat, 11 Apr 2026 12:08:15 -0700
Subject: [PATCH 3/7] fix(trigger): fix Google Sheets trigger header detection
and row index tracking (#4109)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(trigger): auto-detect header row and rename lastKnownRowCount to lastIndexChecked
- Replace hardcoded !1:1 header fetch with detectHeaderRow(), which scans
the first 10 rows and returns the first non-empty row as headers. This
fixes row: null / headers: [] when a sheet has blank rows or a title row
above the actual column headers (e.g. headers in row 3).
- Rename lastKnownRowCount → lastIndexChecked in GoogleSheetsWebhookConfig
and all usage sites to clarify that the value is a row index pointer, not
a total count.
- Remove config parameter from processRows() since it was unused after the
includeHeaders flag was removed.
* fix(trigger): combine sheet state fetch, skip header/blank rows from data emission
- Replace separate getDataRowCount() + detectHeaderRow() with a single
fetchSheetState() call that returns rowCount, headers, and headerRowIndex
from one A:Z fetch. Saves one Sheets API round-trip per poll cycle when
new rows are detected.
- Use headerRowIndex to compute adjustedStartRow, preventing the header row
(and any blank rows above it) from being emitted as data events when
lastIndexChecked was seeded from an empty sheet.
- Handle the edge case where the entire batch falls within the header/blank
window by advancing the pointer and returning early without fetching rows.
- Skip empty rows (row.length === 0) in processRows rather than firing a
workflow run with no meaningful data.
* fix(trigger): preserve lastModifiedTime when remaining rows exist after header skip
When all rows in a batch fall within the header/blank window (adjustedStartRow
> endRow), the early return was unconditionally updating lastModifiedTime to the
current value. If there were additional rows beyond the batch cap, the next
Drive pre-check would see an unchanged modifiedTime and skip polling entirely,
leaving those rows unprocessed. Mirror the hasRemainingOrFailed pattern from the
normal processing path.
* chore(trigger): remove verbose inline comments from google-sheets poller
* fix(trigger): revert to full-width A:Z fetch for correct row count and consistent column scope
* fix(trigger): don't count skipped empty rows as processed
---
.../sim/lib/webhooks/polling/google-sheets.ts | 184 +++++++++---------
1 file changed, 94 insertions(+), 90 deletions(-)
diff --git a/apps/sim/lib/webhooks/polling/google-sheets.ts b/apps/sim/lib/webhooks/polling/google-sheets.ts
index 2b79b20697e..1aaf9bd25f1 100644
--- a/apps/sim/lib/webhooks/polling/google-sheets.ts
+++ b/apps/sim/lib/webhooks/polling/google-sheets.ts
@@ -10,6 +10,9 @@ import { processPolledWebhookEvent } from '@/lib/webhooks/processor'
const MAX_ROWS_PER_POLL = 100
+/** Maximum number of leading rows to scan when auto-detecting the header row. */
+const HEADER_SCAN_ROWS = 10
+
type ValueRenderOption = 'FORMATTED_VALUE' | 'UNFORMATTED_VALUE' | 'FORMULA'
type DateTimeRenderOption = 'SERIAL_NUMBER' | 'FORMATTED_STRING'
@@ -20,7 +23,8 @@ interface GoogleSheetsWebhookConfig {
manualSheetName?: string
valueRenderOption?: ValueRenderOption
dateTimeRenderOption?: DateTimeRenderOption
- lastKnownRowCount?: number
+ /** 1-indexed row number of the last row seeded or processed. */
+ lastIndexChecked?: number
lastModifiedTime?: string
lastCheckedTimestamp?: string
maxRowsPerPoll?: number
@@ -63,7 +67,6 @@ export const googleSheetsPollingHandler: PollingProviderHandler = {
return 'failure'
}
- // Pre-check: use Drive API to see if the file was modified since last poll
const { unchanged: skipPoll, currentModifiedTime } = await isDriveFileUnchanged(
accessToken,
spreadsheetId,
@@ -83,21 +86,29 @@ export const googleSheetsPollingHandler: PollingProviderHandler = {
return 'success'
}
- // Fetch current row count via column A
- const currentRowCount = await getDataRowCount(
+ const valueRender = config.valueRenderOption || 'FORMATTED_VALUE'
+ const dateTimeRender = config.dateTimeRenderOption || 'SERIAL_NUMBER'
+
+ const {
+ rowCount: currentRowCount,
+ headers,
+ headerRowIndex,
+ } = await fetchSheetState(
accessToken,
spreadsheetId,
sheetName,
+ valueRender,
+ dateTimeRender,
requestId,
logger
)
// First poll: seed state, emit nothing
- if (config.lastKnownRowCount === undefined) {
+ if (config.lastIndexChecked === undefined) {
await updateWebhookProviderConfig(
webhookId,
{
- lastKnownRowCount: currentRowCount,
+ lastIndexChecked: currentRowCount,
lastModifiedTime: currentModifiedTime ?? config.lastModifiedTime,
lastCheckedTimestamp: now.toISOString(),
},
@@ -105,22 +116,21 @@ export const googleSheetsPollingHandler: PollingProviderHandler = {
)
await markWebhookSuccess(webhookId, logger)
logger.info(
- `[${requestId}] First poll for webhook ${webhookId}, seeded row count: ${currentRowCount}`
+ `[${requestId}] First poll for webhook ${webhookId}, seeded row index: ${currentRowCount}`
)
return 'success'
}
- // Rows deleted or unchanged
- if (currentRowCount <= config.lastKnownRowCount) {
- if (currentRowCount < config.lastKnownRowCount) {
+ if (currentRowCount <= config.lastIndexChecked) {
+ if (currentRowCount < config.lastIndexChecked) {
logger.warn(
- `[${requestId}] Row count decreased from ${config.lastKnownRowCount} to ${currentRowCount} for webhook ${webhookId}`
+ `[${requestId}] Row count decreased from ${config.lastIndexChecked} to ${currentRowCount} for webhook ${webhookId}`
)
}
await updateWebhookProviderConfig(
webhookId,
{
- lastKnownRowCount: currentRowCount,
+ lastIndexChecked: currentRowCount,
lastModifiedTime: currentModifiedTime ?? config.lastModifiedTime,
lastCheckedTimestamp: now.toISOString(),
},
@@ -131,38 +141,47 @@ export const googleSheetsPollingHandler: PollingProviderHandler = {
return 'success'
}
- // New rows detected
- const newRowCount = currentRowCount - config.lastKnownRowCount
+ const newRowCount = currentRowCount - config.lastIndexChecked
const maxRows = config.maxRowsPerPoll || MAX_ROWS_PER_POLL
const rowsToFetch = Math.min(newRowCount, maxRows)
- const startRow = config.lastKnownRowCount + 1
- const endRow = config.lastKnownRowCount + rowsToFetch
+ const startRow = config.lastIndexChecked + 1
+ const endRow = config.lastIndexChecked + rowsToFetch
+
+ // Skip past the header row (and any blank rows above it) so it is never
+ // emitted as a data event.
+ const adjustedStartRow =
+ headerRowIndex > 0 ? Math.max(startRow, headerRowIndex + 1) : startRow
logger.info(
- `[${requestId}] Found ${newRowCount} new rows for webhook ${webhookId}, processing rows ${startRow}-${endRow}`
+ `[${requestId}] Found ${newRowCount} new rows for webhook ${webhookId}, processing rows ${adjustedStartRow}-${endRow}`
)
- // Resolve render options
- const valueRender = config.valueRenderOption || 'FORMATTED_VALUE'
- const dateTimeRender = config.dateTimeRenderOption || 'SERIAL_NUMBER'
-
- const headers = await fetchHeaderRow(
- accessToken,
- spreadsheetId,
- sheetName,
- valueRender,
- dateTimeRender,
- requestId,
- logger
- )
+ // Entire batch is header/blank rows — advance pointer and skip fetch.
+ if (adjustedStartRow > endRow) {
+ const hasRemainingRows = rowsToFetch < newRowCount
+ await updateWebhookProviderConfig(
+ webhookId,
+ {
+ lastIndexChecked: config.lastIndexChecked + rowsToFetch,
+ lastModifiedTime: hasRemainingRows
+ ? config.lastModifiedTime
+ : (currentModifiedTime ?? config.lastModifiedTime),
+ lastCheckedTimestamp: now.toISOString(),
+ },
+ logger
+ )
+ await markWebhookSuccess(webhookId, logger)
+ logger.info(
+ `[${requestId}] Batch ${startRow}-${endRow} contained only header/blank rows for webhook ${webhookId}, advancing pointer`
+ )
+ return 'success'
+ }
- // Fetch new rows — startRow/endRow are already 1-indexed sheet row numbers
- // because lastKnownRowCount includes the header row
const newRows = await fetchRowRange(
accessToken,
spreadsheetId,
sheetName,
- startRow,
+ adjustedStartRow,
endRow,
valueRender,
dateTimeRender,
@@ -173,10 +192,9 @@ export const googleSheetsPollingHandler: PollingProviderHandler = {
const { processedCount, failedCount } = await processRows(
newRows,
headers,
- startRow,
+ adjustedStartRow,
spreadsheetId,
sheetName,
- config,
webhookData,
workflowData,
requestId,
@@ -184,12 +202,12 @@ export const googleSheetsPollingHandler: PollingProviderHandler = {
)
const rowsAdvanced = failedCount > 0 ? 0 : rowsToFetch
- const newLastKnownRowCount = config.lastKnownRowCount + rowsAdvanced
+ const newLastIndexChecked = config.lastIndexChecked + rowsAdvanced
const hasRemainingOrFailed = rowsAdvanced < newRowCount
await updateWebhookProviderConfig(
webhookId,
{
- lastKnownRowCount: newLastKnownRowCount,
+ lastIndexChecked: newLastIndexChecked,
lastModifiedTime: hasRemainingOrFailed
? config.lastModifiedTime
: (currentModifiedTime ?? config.lastModifiedTime),
@@ -256,20 +274,32 @@ async function getDriveFileModifiedTime(
}
}
-async function getDataRowCount(
+/**
+ * Fetches the sheet (A:Z) and returns the row count, auto-detected headers,
+ * and the 1-indexed header row number in a single API call.
+ *
+ * The Sheets API omits trailing empty rows, so `rows.length` equals the last
+ * non-empty row in columns A–Z. Header detection scans the first
+ * {@link HEADER_SCAN_ROWS} rows for the first non-empty row. Returns
+ * `headerRowIndex = 0` when no header is found within the scan window.
+ */
+async function fetchSheetState(
accessToken: string,
spreadsheetId: string,
sheetName: string,
+ valueRenderOption: ValueRenderOption,
+ dateTimeRenderOption: DateTimeRenderOption,
requestId: string,
logger: ReturnType
-): Promise {
+): Promise<{ rowCount: number; headers: string[]; headerRowIndex: number }> {
const encodedSheet = encodeURIComponent(sheetName)
- // Fetch all rows across columns A–Z with majorDimension=ROWS so the API
- // returns one entry per row that has ANY non-empty cell. Rows where column A
- // is empty but other columns have data are included, whereas the previous
- // column-A-only approach silently missed them. The returned array length
- // equals the 1-indexed row number of the last row with data.
- const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodedSheet}!A:Z?majorDimension=ROWS&fields=values`
+ const params = new URLSearchParams({
+ majorDimension: 'ROWS',
+ fields: 'values',
+ valueRenderOption,
+ dateTimeRenderOption,
+ })
+ const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodedSheet}!A:Z?${params.toString()}`
const response = await fetch(url, {
headers: { Authorization: `Bearer ${accessToken}` },
@@ -278,61 +308,32 @@ async function getDataRowCount(
if (!response.ok) {
const status = response.status
const errorData = await response.json().catch(() => ({}))
-
if (status === 403 || status === 429) {
throw new Error(
`Sheets API rate limit (${status}) — skipping to retry next poll cycle: ${JSON.stringify(errorData)}`
)
}
-
throw new Error(
- `Failed to fetch row count: ${status} ${response.statusText} - ${JSON.stringify(errorData)}`
+ `Failed to fetch sheet state: ${status} ${response.statusText} - ${JSON.stringify(errorData)}`
)
}
const data = await response.json()
- // values is [[row1col1, row1col2, ...], [row2col1, ...], ...] when majorDimension=ROWS.
- // The Sheets API omits trailing empty rows, so the array length is the last
- // non-empty row index (1-indexed), which is exactly what we need.
- const rows = data.values as string[][] | undefined
- return rows?.length ?? 0
-}
+ const rows = (data.values as string[][] | undefined) ?? []
+ const rowCount = rows.length
-async function fetchHeaderRow(
- accessToken: string,
- spreadsheetId: string,
- sheetName: string,
- valueRenderOption: ValueRenderOption,
- dateTimeRenderOption: DateTimeRenderOption,
- requestId: string,
- logger: ReturnType
-): Promise {
- const encodedSheet = encodeURIComponent(sheetName)
- const params = new URLSearchParams({
- fields: 'values',
- valueRenderOption,
- dateTimeRenderOption,
- })
- const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodedSheet}!1:1?${params.toString()}`
-
- const response = await fetch(url, {
- headers: { Authorization: `Bearer ${accessToken}` },
- })
-
- if (!response.ok) {
- const status = response.status
- if (status === 403 || status === 429) {
- const errorData = await response.json().catch(() => ({}))
- throw new Error(
- `Sheets API rate limit (${status}) fetching header row — skipping to retry next poll cycle: ${JSON.stringify(errorData)}`
- )
+ let headers: string[] = []
+ let headerRowIndex = 0
+ for (let i = 0; i < Math.min(rows.length, HEADER_SCAN_ROWS); i++) {
+ const row = rows[i]
+ if (row?.some((cell) => cell !== '')) {
+ headers = row
+ headerRowIndex = i + 1
+ break
}
- logger.warn(`[${requestId}] Failed to fetch header row, proceeding without headers`)
- return []
}
- const data = await response.json()
- return (data.values?.[0] as string[]) ?? []
+ return { rowCount, headers, headerRowIndex }
}
async function fetchRowRange(
@@ -361,13 +362,11 @@ async function fetchRowRange(
if (!response.ok) {
const status = response.status
const errorData = await response.json().catch(() => ({}))
-
if (status === 403 || status === 429) {
throw new Error(
`Sheets API rate limit (${status}) — skipping to retry next poll cycle: ${JSON.stringify(errorData)}`
)
}
-
throw new Error(
`Failed to fetch rows ${startRow}-${endRow}: ${status} ${response.statusText} - ${JSON.stringify(errorData)}`
)
@@ -383,7 +382,6 @@ async function processRows(
startRowIndex: number,
spreadsheetId: string,
sheetName: string,
- config: GoogleSheetsWebhookConfig,
webhookData: PollWebhookContext['webhookData'],
workflowData: PollWebhookContext['workflowData'],
requestId: string,
@@ -394,7 +392,13 @@ async function processRows(
for (let i = 0; i < rows.length; i++) {
const row = rows[i]
- const rowNumber = startRowIndex + i // startRowIndex is already the 1-indexed sheet row
+ const rowNumber = startRowIndex + i
+
+ // Skip empty rows — don't fire a workflow run with no data.
+ if (!row || row.length === 0) {
+ logger.info(`[${requestId}] Skipping empty row ${rowNumber} for webhook ${webhookData.id}`)
+ continue
+ }
try {
await pollingIdempotency.executeWithIdempotency(
From 6a4f5f20741d830bf6112b25b52fda664ba2f1ec Mon Sep 17 00:00:00 2001
From: Waleed
Date: Sat, 11 Apr 2026 15:04:08 -0700
Subject: [PATCH 4/7] fix(trigger): handle Drive rate limits, 410 page token
expiry, and clean up comments (#4112)
* fix(trigger): handle Drive rate limits, 410 page token expiry, and clean up comments
* fix(trigger): treat Drive rate limits as success to preserve failure budget
* fix(trigger): distinguish Drive 403 rate limits from permission errors, preserve knownFileIds on 410 re-seed
---
apps/sim/lib/webhooks/polling/google-drive.ts | 76 ++++++++++++-------
1 file changed, 50 insertions(+), 26 deletions(-)
diff --git a/apps/sim/lib/webhooks/polling/google-drive.ts b/apps/sim/lib/webhooks/polling/google-drive.ts
index d57c19fa50b..6dcdd9cdffb 100644
--- a/apps/sim/lib/webhooks/polling/google-drive.ts
+++ b/apps/sim/lib/webhooks/polling/google-drive.ts
@@ -89,13 +89,12 @@ export const googleDrivePollingHandler: PollingProviderHandler = {
const config = webhookData.providerConfig as unknown as GoogleDriveWebhookConfig
- // First poll: get startPageToken and seed state
+ // First poll (or re-seed after 410): seed page token, preserve any existing known file IDs.
if (!config.pageToken) {
const startPageToken = await getStartPageToken(accessToken, config, requestId, logger)
-
await updateWebhookProviderConfig(
webhookId,
- { pageToken: startPageToken, knownFileIds: [] },
+ { pageToken: startPageToken, knownFileIds: config.knownFileIds ?? [] },
logger
)
await markWebhookSuccess(webhookId, logger)
@@ -105,7 +104,6 @@ export const googleDrivePollingHandler: PollingProviderHandler = {
return 'success'
}
- // Fetch changes since last pageToken
const { changes, newStartPageToken } = await fetchChanges(
accessToken,
config,
@@ -120,7 +118,6 @@ export const googleDrivePollingHandler: PollingProviderHandler = {
return 'success'
}
- // Filter changes client-side (folder, MIME type, trashed)
const filteredChanges = filterChanges(changes, config)
if (!filteredChanges.length) {
@@ -145,11 +142,6 @@ export const googleDrivePollingHandler: PollingProviderHandler = {
logger
)
- // Update state: new pageToken and rolling knownFileIds.
- // Newest IDs are placed first so that when the set exceeds MAX_KNOWN_FILE_IDS,
- // the oldest (least recently seen) IDs are evicted. Recent files are more
- // likely to be modified again, so keeping them prevents misclassifying a
- // repeat modification as a "created" event.
const existingKnownIds = config.knownFileIds || []
const mergedKnownIds = [...new Set([...newKnownFileIds, ...existingKnownIds])].slice(
0,
@@ -180,6 +172,21 @@ export const googleDrivePollingHandler: PollingProviderHandler = {
)
return 'success'
} catch (error) {
+ if (error instanceof Error && error.name === 'DrivePageTokenInvalidError') {
+ await updateWebhookProviderConfig(webhookId, { pageToken: undefined }, logger)
+ await markWebhookSuccess(webhookId, logger)
+ logger.warn(
+ `[${requestId}] Drive page token invalid for webhook ${webhookId}, re-seeding on next poll`
+ )
+ return 'success'
+ }
+ if (error instanceof Error && error.name === 'DriveRateLimitError') {
+ await markWebhookSuccess(webhookId, logger)
+ logger.warn(
+ `[${requestId}] Drive API rate limited for webhook ${webhookId}, skipping to retry next poll cycle`
+ )
+ return 'success'
+ }
logger.error(`[${requestId}] Error processing Google Drive webhook ${webhookId}:`, error)
await markWebhookFailed(webhookId, logger)
return 'failure'
@@ -187,6 +194,16 @@ export const googleDrivePollingHandler: PollingProviderHandler = {
},
}
+const DRIVE_RATE_LIMIT_REASONS = new Set(['rateLimitExceeded', 'userRateLimitExceeded'])
+
+/** Returns true only for quota/rate-limit 403s, not permission errors. */
+function isDriveRateLimitError(status: number, errorData: Record): boolean {
+ if (status !== 403) return false
+ const reason = (errorData as { error?: { errors?: { reason?: string }[] } })?.error?.errors?.[0]
+ ?.reason
+ return reason !== undefined && DRIVE_RATE_LIMIT_REASONS.has(reason)
+}
+
async function getStartPageToken(
accessToken: string,
config: GoogleDriveWebhookConfig,
@@ -204,9 +221,15 @@ async function getStartPageToken(
})
if (!response.ok) {
+ const status = response.status
const errorData = await response.json().catch(() => ({}))
+ if (status === 429 || isDriveRateLimitError(status, errorData)) {
+ const err = new Error(`Drive API rate limit (${status}): ${JSON.stringify(errorData)}`)
+ err.name = 'DriveRateLimitError'
+ throw err
+ }
throw new Error(
- `Failed to get startPageToken: ${response.status} - ${JSON.stringify(errorData)}`
+ `Failed to get Drive start page token: ${status} - ${JSON.stringify(errorData)}`
)
}
@@ -227,7 +250,6 @@ async function fetchChanges(
const maxFiles = config.maxFilesPerPoll || MAX_FILES_PER_POLL
let pages = 0
- // eslint-disable-next-line no-constant-condition
while (true) {
pages++
const params = new URLSearchParams({
@@ -248,8 +270,19 @@ async function fetchChanges(
})
if (!response.ok) {
+ const status = response.status
const errorData = await response.json().catch(() => ({}))
- throw new Error(`Failed to fetch changes: ${response.status} - ${JSON.stringify(errorData)}`)
+ if (status === 410) {
+ const err = new Error('Drive page token is no longer valid')
+ err.name = 'DrivePageTokenInvalidError'
+ throw err
+ }
+ if (status === 429 || isDriveRateLimitError(status, errorData)) {
+ const err = new Error(`Drive API rate limit (${status}): ${JSON.stringify(errorData)}`)
+ err.name = 'DriveRateLimitError'
+ throw err
+ }
+ throw new Error(`Failed to fetch Drive changes: ${status} - ${JSON.stringify(errorData)}`)
}
const data = await response.json()
@@ -274,12 +307,9 @@ async function fetchChanges(
currentPageToken = data.nextPageToken as string
}
+ // When allChanges exceeds maxFiles (multi-page overshoot), resume mid-list via lastNextPageToken.
+ // Otherwise resume from newStartPageToken (end of change list) or lastNextPageToken (MAX_PAGES hit).
const slicingOccurs = allChanges.length > maxFiles
- // Drive API guarantees exactly one of nextPageToken or newStartPageToken per response.
- // Slicing case: prefer lastNextPageToken (mid-list resume); fall back to newStartPageToken
- // (guaranteed on final page when hasMore was false). Non-slicing case: prefer newStartPageToken
- // (guaranteed when loop exhausted all pages); fall back to lastNextPageToken (when loop exited
- // early due to MAX_PAGES with hasMore still true).
const resumeToken = slicingOccurs
? (lastNextPageToken ?? newStartPageToken!)
: (newStartPageToken ?? lastNextPageToken!)
@@ -292,16 +322,13 @@ function filterChanges(
config: GoogleDriveWebhookConfig
): DriveChangeEntry[] {
return changes.filter((change) => {
- // Always include removals (deletions)
if (change.removed) return true
const file = change.file
if (!file) return false
- // Exclude trashed files
if (file.trashed) return false
- // Folder filter: check if file is in the specified folder
const folderId = config.folderId || config.manualFolderId
if (folderId) {
if (!file.parents || !file.parents.includes(folderId)) {
@@ -309,9 +336,7 @@ function filterChanges(
}
}
- // MIME type filter
if (config.mimeTypeFilter) {
- // Support prefix matching (e.g., "image/" matches "image/png", "image/jpeg")
if (config.mimeTypeFilter.endsWith('/')) {
if (!file.mimeType.startsWith(config.mimeTypeFilter)) {
return false
@@ -339,7 +364,6 @@ async function processChanges(
const knownFileIdsSet = new Set(config.knownFileIds || [])
for (const change of changes) {
- // Determine event type before idempotency to avoid caching filter decisions
let eventType: 'created' | 'modified' | 'deleted'
if (change.removed) {
eventType = 'deleted'
@@ -349,12 +373,12 @@ async function processChanges(
eventType = 'modified'
}
- // Track file as known regardless of filter (for future create/modify distinction)
+ // Track file as known regardless of filter so future changes are correctly classified
if (!change.removed) {
newKnownFileIds.push(change.fileId)
}
- // Client-side event type filter — skip before idempotency so filtered events aren't cached
+ // Apply event type filter before idempotency so filtered events aren't cached
const filter = config.eventTypeFilter
if (filter) {
const skip = filter === 'created_or_modified' ? eventType === 'deleted' : eventType !== filter
From 30c5e82ab0c5e3b751ddf32c470316baab2bd6df Mon Sep 17 00:00:00 2001
From: Waleed
Date: Sat, 11 Apr 2026 16:15:48 -0700
Subject: [PATCH 5/7] feat(ee): add enterprise audit logs settings page (#4111)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(ee): add enterprise audit logs settings page with server-side search
Add a new audit logs page under enterprise settings that displays all
actions captured via recordAudit. Includes server-side search, resource
type filtering, date range selection, and cursor-based pagination.
- Add internal API route (app/api/audit-logs) with session auth
- Extract shared query logic (buildFilterConditions, buildOrgScopeCondition,
queryAuditLogs) into app/api/v1/audit-logs/query.ts
- Refactor v1 and admin audit log routes to use shared query module
- Add React Query hook with useInfiniteQuery and cursor pagination
- Add audit logs UI with debounced search, combobox filters, expandable rows
- Gate behind requiresHosted + requiresEnterprise navigation flags
- Place all enterprise audit log code in ee/audit-logs/
Co-Authored-By: Claude Opus 4.6
* lint
* fix(ee): fix build error and address PR review comments
- Fix import path: @/lib/utils → @/lib/core/utils/cn
- Guard against empty orgMemberIds array in buildOrgScopeCondition
- Skip debounce effect on mount when search is already synced
Co-Authored-By: Claude Opus 4.6
* lint
* fix(ee): fix type error with unknown metadata in JSX expression
Use ternary instead of && chain to prevent unknown type from being
returned as ReactNode.
Co-Authored-By: Claude Opus 4.6
* fix(ee): align skeleton filter width with actual component layout
Co-Authored-By: Claude Opus 4.6
* lint
* feat(audit): add audit logging for passwords, credentials, and schedules
- Add PASSWORD_RESET_REQUESTED audit on forget-password with user lookup
- Add CREDENTIAL_CREATED/UPDATED/DELETED audit on credential CRUD routes
with metadata (credentialType, providerId, updatedFields, envKey)
- Add SCHEDULE_CREATED audit on schedule creation with cron/timezone metadata
- Fix SCHEDULE_DELETED (was incorrectly using SCHEDULE_UPDATED for deletes)
- Enhance existing schedule update/disable/reactivate audit with structured
metadata (operation, updatedFields, sourceType, previousStatus)
- Add CREDENTIAL resource type and Credential filter option to audit logs UI
- Enhance password reset completed description with user email
Co-Authored-By: Claude Opus 4.6
* fix(audit): align metadata with established recordAudit patterns
- Add actorName/actorEmail to all new credential and schedule audit calls
to match the established pattern (e.g., api-keys, byok-keys, knowledge)
- Add resourceId and resourceName to forget-password audit call
- Enhance forget-password description with user email
Co-Authored-By: Claude Opus 4.6
* fix(testing): sync audit mock with new AuditAction and AuditResourceType entries
Co-Authored-By: Claude Opus 4.6
* refactor(audit-logs): derive resource type filter from AuditResourceType
Instead of maintaining a separate hardcoded list, the filter dropdown
now derives its options directly from the AuditResourceType const object.
Co-Authored-By: Claude Opus 4.6
* feat(audit): enrich all recordAudit calls with structured metadata
- Move resource type filter options to ee/audit-logs/constants.ts
(derived from AuditResourceType, no separate list to maintain)
- Remove export from internal cursor helpers in query.ts
- Add 5 new AuditAction entries: BYOK_KEY_UPDATED, ENVIRONMENT_DELETED,
INVITATION_RESENT, WORKSPACE_UPDATED, ORG_INVITATION_RESENT
- Enrich ~80 recordAudit calls across the codebase with structured
metadata (knowledge bases, connectors, documents, workspaces, members,
invitations, workflows, deployments, templates, MCP servers, credential
sets, organizations, permission groups, files, tables, notifications,
copilot operations)
- Sync audit mock with all new entries
Co-Authored-By: Claude Opus 4.6
* fix(audit): remove redundant metadata fields duplicating top-level audit fields
Remove metadata entries that duplicate resourceName, workspaceId, or
other top-level recordAudit fields. Also remove noisy fileNames arrays
from bulk document upload audits (kept fileCount).
Co-Authored-By: Claude Opus 4.6
* fix(audit): split audit types from server-only log module
Extract AuditAction, AuditResourceType, and their types into
lib/audit/types.ts (client-safe, no @sim/db dependency). The
server-only recordAudit stays in log.ts and re-exports the types
for backwards compatibility. constants.ts now imports from types.ts
directly, breaking the postgres -> tls client bundle chain.
Co-Authored-By: Claude Opus 4.6
* fix(audit): escape LIKE wildcards in audit log search query
Escape %, _, and \ characters in the search parameter before embedding
in the LIKE pattern to prevent unintended broad matches.
Co-Authored-By: Claude Opus 4.6
* fix(audit): use actual deletedCount in bulk API key revoke description
The description was using keys.length (requested count) instead of
deletedCount (actual count), which could differ if some keys didn't
exist.
Co-Authored-By: Claude Opus 4.6
* fix(audit-logs): fix OAuth label displaying as "Oauth" in filter dropdown
ACRONYMS set stored 'OAuth' but lookup used toUpperCase() producing
'OAUTH' which never matched. Now store all acronyms uppercase and use
a display override map for special casing like OAuth.
Co-Authored-By: Claude Opus 4.6
---------
Co-authored-by: Claude Opus 4.6
---
apps/sim/app/api/audit-logs/route.ts | 71 +++++
.../sim/app/api/auth/forget-password/route.ts | 24 ++
apps/sim/app/api/billing/credits/route.ts | 6 +-
apps/sim/app/api/chat/manage/[id]/route.ts | 6 +
.../[id]/invite/[invitationId]/route.ts | 7 +-
.../api/credential-sets/[id]/invite/route.ts | 7 +-
.../api/credential-sets/[id]/members/route.ts | 7 +-
.../sim/app/api/credential-sets/[id]/route.ts | 8 +
.../credential-sets/invite/[token]/route.ts | 7 +-
.../api/credential-sets/memberships/route.ts | 1 +
apps/sim/app/api/credential-sets/route.ts | 1 +
apps/sim/app/api/credentials/[id]/route.ts | 63 +++++
apps/sim/app/api/credentials/route.ts | 18 ++
apps/sim/app/api/environment/route.ts | 9 +-
apps/sim/app/api/folders/route.ts | 8 +-
apps/sim/app/api/form/manage/[id]/route.ts | 11 +-
apps/sim/app/api/form/route.ts | 1 +
.../[connectorId]/documents/route.ts | 16 +-
.../[id]/connectors/[connectorId]/route.ts | 14 +-
.../connectors/[connectorId]/sync/route.ts | 8 +-
.../api/knowledge/[id]/connectors/route.ts | 8 +-
.../[id]/documents/[documentId]/route.ts | 21 +-
.../app/api/knowledge/[id]/documents/route.ts | 3 +-
.../knowledge/[id]/documents/upsert/route.ts | 3 +
.../app/api/knowledge/[id]/restore/route.ts | 3 +
apps/sim/app/api/knowledge/[id]/route.ts | 17 ++
apps/sim/app/api/knowledge/route.ts | 11 +-
apps/sim/app/api/mcp/servers/[id]/route.ts | 8 +
apps/sim/app/api/mcp/servers/route.ts | 15 +-
.../api/mcp/workflow-servers/[id]/route.ts | 6 +
.../[id]/tools/[toolId]/route.ts | 9 +-
.../mcp/workflow-servers/[id]/tools/route.ts | 8 +-
.../sim/app/api/mcp/workflow-servers/route.ts | 7 +
.../[id]/invitations/[invitationId]/route.ts | 14 +
.../organizations/[id]/invitations/route.ts | 10 +-
.../api/organizations/[id]/members/route.ts | 1 +
apps/sim/app/api/organizations/route.ts | 1 +
.../app/api/permission-groups/[id]/route.ts | 7 +
apps/sim/app/api/permission-groups/route.ts | 1 +
apps/sim/app/api/schedules/[id]/route.ts | 56 ++--
apps/sim/app/api/schedules/route.ts | 20 ++
apps/sim/app/api/skills/route.ts | 6 +
.../app/api/table/[tableId]/restore/route.ts | 4 +
apps/sim/app/api/templates/[id]/route.ts | 16 ++
apps/sim/app/api/templates/route.ts | 8 +
apps/sim/app/api/tools/custom/route.ts | 9 +-
apps/sim/app/api/v1/admin/audit-logs/route.ts | 37 ++-
apps/sim/app/api/v1/audit-logs/query.ts | 146 ++++++++++
apps/sim/app/api/v1/audit-logs/route.ts | 106 ++-----
apps/sim/app/api/v1/files/[fileId]/route.ts | 1 +
apps/sim/app/api/v1/files/route.ts | 1 +
.../[id]/documents/[documentId]/route.ts | 1 +
.../api/v1/knowledge/[id]/documents/route.ts | 1 +
apps/sim/app/api/v1/knowledge/[id]/route.ts | 1 +
apps/sim/app/api/v1/knowledge/route.ts | 1 +
apps/sim/app/api/v1/tables/route.ts | 1 +
apps/sim/app/api/webhooks/[id]/route.ts | 10 +-
apps/sim/app/api/webhooks/route.ts | 7 +-
.../deployments/[version]/revert/route.ts | 3 +
.../app/api/workflows/[id]/duplicate/route.ts | 6 +-
.../app/api/workflows/[id]/restore/route.ts | 4 +
.../app/api/workflows/[id]/variables/route.ts | 6 +-
apps/sim/app/api/workflows/route.ts | 9 +-
.../workspaces/[id]/api-keys/[keyId]/route.ts | 13 +-
.../app/api/workspaces/[id]/api-keys/route.ts | 6 +-
.../api/workspaces/[id]/byok-keys/route.ts | 14 +
.../api/workspaces/[id]/environment/route.ts | 24 +-
.../[id]/files/[fileId]/content/route.ts | 2 +
.../workspaces/[id]/files/[fileId]/route.ts | 1 +
.../app/api/workspaces/[id]/files/route.ts | 1 +
.../notifications/[notificationId]/route.ts | 11 +
.../workspaces/[id]/notifications/route.ts | 11 +
.../api/workspaces/[id]/permissions/route.ts | 12 +-
apps/sim/app/api/workspaces/[id]/route.ts | 31 ++
.../invitations/[invitationId]/route.ts | 32 ++-
.../app/api/workspaces/invitations/route.ts | 7 +-
.../app/api/workspaces/members/[id]/route.ts | 8 +-
apps/sim/app/api/workspaces/route.ts | 2 +-
.../settings/[section]/settings.tsx | 6 +
.../[workspaceId]/settings/navigation.ts | 10 +
.../components/audit-logs-skeleton.tsx | 27 ++
.../ee/audit-logs/components/audit-logs.tsx | 267 ++++++++++++++++++
apps/sim/ee/audit-logs/constants.ts | 24 ++
apps/sim/ee/audit-logs/hooks/audit-logs.ts | 58 ++++
apps/sim/lib/audit/log.ts | 208 +-------------
apps/sim/lib/audit/types.ts | 214 ++++++++++++++
apps/sim/lib/auth/auth.ts | 3 +-
.../tool-executor/deployment-tools/deploy.ts | 3 +
.../tool-executor/deployment-tools/manage.ts | 13 +-
.../orchestrator/tool-executor/index.ts | 14 +
.../tool-executor/workflow-tools/mutations.ts | 8 +-
.../workflows/orchestration/chat-deploy.ts | 19 +-
.../sim/lib/workflows/orchestration/deploy.ts | 14 +-
packages/testing/src/mocks/audit.mock.ts | 11 +
94 files changed, 1593 insertions(+), 387 deletions(-)
create mode 100644 apps/sim/app/api/audit-logs/route.ts
create mode 100644 apps/sim/app/api/v1/audit-logs/query.ts
create mode 100644 apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx
create mode 100644 apps/sim/ee/audit-logs/components/audit-logs.tsx
create mode 100644 apps/sim/ee/audit-logs/constants.ts
create mode 100644 apps/sim/ee/audit-logs/hooks/audit-logs.ts
create mode 100644 apps/sim/lib/audit/types.ts
diff --git a/apps/sim/app/api/audit-logs/route.ts b/apps/sim/app/api/audit-logs/route.ts
new file mode 100644
index 00000000000..3be8c2dc3b6
--- /dev/null
+++ b/apps/sim/app/api/audit-logs/route.ts
@@ -0,0 +1,71 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import { getSession } from '@/lib/auth'
+import { validateEnterpriseAuditAccess } from '@/app/api/v1/audit-logs/auth'
+import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format'
+import {
+ buildFilterConditions,
+ buildOrgScopeCondition,
+ queryAuditLogs,
+} from '@/app/api/v1/audit-logs/query'
+
+const logger = createLogger('AuditLogsAPI')
+
+export const dynamic = 'force-dynamic'
+
+export async function GET(request: Request) {
+ try {
+ const session = await getSession()
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const authResult = await validateEnterpriseAuditAccess(session.user.id)
+ if (!authResult.success) {
+ return authResult.response
+ }
+
+ const { orgMemberIds } = authResult.context
+
+ const { searchParams } = new URL(request.url)
+ const search = searchParams.get('search')?.trim() || undefined
+ const startDate = searchParams.get('startDate') || undefined
+ const endDate = searchParams.get('endDate') || undefined
+ const includeDeparted = searchParams.get('includeDeparted') === 'true'
+ const limit = Math.min(Math.max(Number(searchParams.get('limit')) || 50, 1), 100)
+ const cursor = searchParams.get('cursor') || undefined
+
+ if (startDate && Number.isNaN(Date.parse(startDate))) {
+ return NextResponse.json({ error: 'Invalid startDate format' }, { status: 400 })
+ }
+ if (endDate && Number.isNaN(Date.parse(endDate))) {
+ return NextResponse.json({ error: 'Invalid endDate format' }, { status: 400 })
+ }
+
+ const scopeCondition = await buildOrgScopeCondition(orgMemberIds, includeDeparted)
+ const filterConditions = buildFilterConditions({
+ action: searchParams.get('action') || undefined,
+ resourceType: searchParams.get('resourceType') || undefined,
+ actorId: searchParams.get('actorId') || undefined,
+ search,
+ startDate,
+ endDate,
+ })
+
+ const { data, nextCursor } = await queryAuditLogs(
+ [scopeCondition, ...filterConditions],
+ limit,
+ cursor
+ )
+
+ return NextResponse.json({
+ success: true,
+ data: data.map(formatAuditLogEntry),
+ nextCursor,
+ })
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : 'Unknown error'
+ logger.error('Audit logs fetch error', { error: message })
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
diff --git a/apps/sim/app/api/auth/forget-password/route.ts b/apps/sim/app/api/auth/forget-password/route.ts
index e8f05ecfcf1..9db6eef95c9 100644
--- a/apps/sim/app/api/auth/forget-password/route.ts
+++ b/apps/sim/app/api/auth/forget-password/route.ts
@@ -1,6 +1,10 @@
+import { db } from '@sim/db'
+import { user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
+import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
+import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { auth } from '@/lib/auth'
import { isSameOrigin } from '@/lib/core/utils/validation'
@@ -51,6 +55,26 @@ export async function POST(request: NextRequest) {
method: 'POST',
})
+ const [existingUser] = await db
+ .select({ id: user.id, name: user.name, email: user.email })
+ .from(user)
+ .where(eq(user.email, email))
+ .limit(1)
+
+ if (existingUser) {
+ recordAudit({
+ actorId: existingUser.id,
+ actorName: existingUser.name,
+ actorEmail: existingUser.email,
+ action: AuditAction.PASSWORD_RESET_REQUESTED,
+ resourceType: AuditResourceType.PASSWORD,
+ resourceId: existingUser.id,
+ resourceName: existingUser.email ?? undefined,
+ description: `Password reset requested for ${existingUser.email}`,
+ request,
+ })
+ }
+
return NextResponse.json({ success: true })
} catch (error) {
logger.error('Error requesting password reset:', { error })
diff --git a/apps/sim/app/api/billing/credits/route.ts b/apps/sim/app/api/billing/credits/route.ts
index 7dfeafb2efe..070b3893133 100644
--- a/apps/sim/app/api/billing/credits/route.ts
+++ b/apps/sim/app/api/billing/credits/route.ts
@@ -64,8 +64,12 @@ export async function POST(request: NextRequest) {
actorEmail: session.user.email,
action: AuditAction.CREDIT_PURCHASED,
resourceType: AuditResourceType.BILLING,
+ resourceId: validation.data.requestId,
description: `Purchased $${validation.data.amount} in credits`,
- metadata: { amount: validation.data.amount, requestId: validation.data.requestId },
+ metadata: {
+ amountDollars: validation.data.amount,
+ requestId: validation.data.requestId,
+ },
request,
})
diff --git a/apps/sim/app/api/chat/manage/[id]/route.ts b/apps/sim/app/api/chat/manage/[id]/route.ts
index c09688c99d6..8cf37410ae0 100644
--- a/apps/sim/app/api/chat/manage/[id]/route.ts
+++ b/apps/sim/app/api/chat/manage/[id]/route.ts
@@ -233,6 +233,12 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
resourceId: chatId,
resourceName: title || existingChatRecord.title,
description: `Updated chat deployment "${title || existingChatRecord.title}"`,
+ metadata: {
+ identifier: updatedIdentifier,
+ authType: updateData.authType || existingChatRecord.authType,
+ workflowId: workflowId || existingChatRecord.workflowId,
+ chatUrl,
+ },
request,
})
diff --git a/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts b/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts
index 9a91b86b8e2..752ebc1a9e7 100644
--- a/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts
+++ b/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts
@@ -159,7 +159,12 @@ export async function POST(
resourceId: id,
resourceName: result.set.name,
description: `Resent credential set invitation to ${invitation.email}`,
- metadata: { invitationId, targetEmail: invitation.email },
+ metadata: {
+ invitationId,
+ targetEmail: invitation.email,
+ providerId: result.set.providerId,
+ credentialSetName: result.set.name,
+ },
request: req,
})
diff --git a/apps/sim/app/api/credential-sets/[id]/invite/route.ts b/apps/sim/app/api/credential-sets/[id]/invite/route.ts
index cd5ebb53015..b9b0ccc4a95 100644
--- a/apps/sim/app/api/credential-sets/[id]/invite/route.ts
+++ b/apps/sim/app/api/credential-sets/[id]/invite/route.ts
@@ -187,7 +187,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
actorEmail: session.user.email ?? undefined,
resourceName: result.set.name,
description: `Created invitation for credential set "${result.set.name}"${email ? ` to ${email}` : ''}`,
- metadata: { targetEmail: email || undefined },
+ metadata: {
+ invitationId: invitation.id,
+ targetEmail: email || undefined,
+ providerId: result.set.providerId,
+ credentialSetName: result.set.name,
+ },
request: req,
})
diff --git a/apps/sim/app/api/credential-sets/[id]/members/route.ts b/apps/sim/app/api/credential-sets/[id]/members/route.ts
index e6ffbaa6262..8ec89923bbe 100644
--- a/apps/sim/app/api/credential-sets/[id]/members/route.ts
+++ b/apps/sim/app/api/credential-sets/[id]/members/route.ts
@@ -197,7 +197,12 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
actorEmail: session.user.email ?? undefined,
resourceName: result.set.name,
description: `Removed member from credential set "${result.set.name}"`,
- metadata: { targetEmail: memberToRemove.email ?? undefined },
+ metadata: {
+ memberId,
+ memberUserId: memberToRemove.userId,
+ targetEmail: memberToRemove.email ?? undefined,
+ providerId: result.set.providerId,
+ },
request: req,
})
diff --git a/apps/sim/app/api/credential-sets/[id]/route.ts b/apps/sim/app/api/credential-sets/[id]/route.ts
index 51110916e93..d522cf9c3df 100644
--- a/apps/sim/app/api/credential-sets/[id]/route.ts
+++ b/apps/sim/app/api/credential-sets/[id]/route.ts
@@ -142,6 +142,13 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
actorEmail: session.user.email ?? undefined,
resourceName: updated?.name ?? result.set.name,
description: `Updated credential set "${updated?.name ?? result.set.name}"`,
+ metadata: {
+ organizationId: result.set.organizationId,
+ providerId: result.set.providerId,
+ updatedFields: Object.keys(updates).filter(
+ (k) => updates[k as keyof typeof updates] !== undefined
+ ),
+ },
request: req,
})
@@ -199,6 +206,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
actorEmail: session.user.email ?? undefined,
resourceName: result.set.name,
description: `Deleted credential set "${result.set.name}"`,
+ metadata: { organizationId: result.set.organizationId, providerId: result.set.providerId },
request: req,
})
diff --git a/apps/sim/app/api/credential-sets/invite/[token]/route.ts b/apps/sim/app/api/credential-sets/invite/[token]/route.ts
index 656d39fdde1..fc3759b0e27 100644
--- a/apps/sim/app/api/credential-sets/invite/[token]/route.ts
+++ b/apps/sim/app/api/credential-sets/invite/[token]/route.ts
@@ -192,7 +192,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
resourceId: invitation.credentialSetId,
resourceName: invitation.credentialSetName,
description: `Accepted credential set invitation`,
- metadata: { invitationId: invitation.id },
+ metadata: {
+ invitationId: invitation.id,
+ credentialSetId: invitation.credentialSetId,
+ providerId: invitation.providerId,
+ credentialSetName: invitation.credentialSetName,
+ },
request: req,
})
diff --git a/apps/sim/app/api/credential-sets/memberships/route.ts b/apps/sim/app/api/credential-sets/memberships/route.ts
index aef704f7b9c..926714b98f9 100644
--- a/apps/sim/app/api/credential-sets/memberships/route.ts
+++ b/apps/sim/app/api/credential-sets/memberships/route.ts
@@ -116,6 +116,7 @@ export async function DELETE(req: NextRequest) {
resourceType: AuditResourceType.CREDENTIAL_SET,
resourceId: credentialSetId,
description: `Left credential set`,
+ metadata: { credentialSetId },
request: req,
})
diff --git a/apps/sim/app/api/credential-sets/route.ts b/apps/sim/app/api/credential-sets/route.ts
index b5166630af9..c120e84b421 100644
--- a/apps/sim/app/api/credential-sets/route.ts
+++ b/apps/sim/app/api/credential-sets/route.ts
@@ -179,6 +179,7 @@ export async function POST(req: Request) {
actorEmail: session.user.email ?? undefined,
resourceName: name,
description: `Created credential set "${name}"`,
+ metadata: { organizationId, providerId, credentialSetName: name },
request: req,
})
diff --git a/apps/sim/app/api/credentials/[id]/route.ts b/apps/sim/app/api/credentials/[id]/route.ts
index 14f2e73142b..c3a61569051 100644
--- a/apps/sim/app/api/credentials/[id]/route.ts
+++ b/apps/sim/app/api/credentials/[id]/route.ts
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
+import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { generateId } from '@/lib/core/utils/uuid'
@@ -166,6 +167,23 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
updates.updatedAt = new Date()
await db.update(credential).set(updates).where(eq(credential.id, id))
+ recordAudit({
+ workspaceId: access.credential.workspaceId,
+ actorId: session.user.id,
+ actorName: session.user.name,
+ actorEmail: session.user.email,
+ action: AuditAction.CREDENTIAL_UPDATED,
+ resourceType: AuditResourceType.CREDENTIAL,
+ resourceId: id,
+ resourceName: access.credential.displayName,
+ description: `Updated ${access.credential.type} credential "${access.credential.displayName}"`,
+ metadata: {
+ credentialType: access.credential.type,
+ updatedFields: Object.keys(updates).filter((k) => k !== 'updatedAt'),
+ },
+ request,
+ })
+
const row = await getCredentialResponse(id, session.user.id)
return NextResponse.json({ credential: row }, { status: 200 })
} catch (error) {
@@ -249,6 +267,20 @@ export async function DELETE(
{ groups: { workspace: access.credential.workspaceId } }
)
+ recordAudit({
+ workspaceId: access.credential.workspaceId,
+ actorId: session.user.id,
+ actorName: session.user.name,
+ actorEmail: session.user.email,
+ action: AuditAction.CREDENTIAL_DELETED,
+ resourceType: AuditResourceType.CREDENTIAL,
+ resourceId: id,
+ resourceName: access.credential.displayName,
+ description: `Deleted personal env credential "${access.credential.envKey}"`,
+ metadata: { credentialType: 'env_personal', envKey: access.credential.envKey },
+ request,
+ })
+
return NextResponse.json({ success: true }, { status: 200 })
}
@@ -302,6 +334,20 @@ export async function DELETE(
{ groups: { workspace: access.credential.workspaceId } }
)
+ recordAudit({
+ workspaceId: access.credential.workspaceId,
+ actorId: session.user.id,
+ actorName: session.user.name,
+ actorEmail: session.user.email,
+ action: AuditAction.CREDENTIAL_DELETED,
+ resourceType: AuditResourceType.CREDENTIAL,
+ resourceId: id,
+ resourceName: access.credential.displayName,
+ description: `Deleted workspace env credential "${access.credential.envKey}"`,
+ metadata: { credentialType: 'env_workspace', envKey: access.credential.envKey },
+ request,
+ })
+
return NextResponse.json({ success: true }, { status: 200 })
}
@@ -318,6 +364,23 @@ export async function DELETE(
{ groups: { workspace: access.credential.workspaceId } }
)
+ recordAudit({
+ workspaceId: access.credential.workspaceId,
+ actorId: session.user.id,
+ actorName: session.user.name,
+ actorEmail: session.user.email,
+ action: AuditAction.CREDENTIAL_DELETED,
+ resourceType: AuditResourceType.CREDENTIAL,
+ resourceId: id,
+ resourceName: access.credential.displayName,
+ description: `Deleted ${access.credential.type} credential "${access.credential.displayName}"`,
+ metadata: {
+ credentialType: access.credential.type,
+ providerId: access.credential.providerId,
+ },
+ request,
+ })
+
return NextResponse.json({ success: true }, { status: 200 })
} catch (error) {
logger.error('Failed to delete credential', error)
diff --git a/apps/sim/app/api/credentials/route.ts b/apps/sim/app/api/credentials/route.ts
index 7d30b63d7b4..0b210325064 100644
--- a/apps/sim/app/api/credentials/route.ts
+++ b/apps/sim/app/api/credentials/route.ts
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
+import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { generateRequestId } from '@/lib/core/utils/request'
@@ -612,6 +613,23 @@ export async function POST(request: NextRequest) {
}
)
+ recordAudit({
+ workspaceId,
+ actorId: session.user.id,
+ actorName: session.user.name,
+ actorEmail: session.user.email,
+ action: AuditAction.CREDENTIAL_CREATED,
+ resourceType: AuditResourceType.CREDENTIAL,
+ resourceId: credentialId,
+ resourceName: resolvedDisplayName,
+ description: `Created ${type} credential "${resolvedDisplayName}"`,
+ metadata: {
+ credentialType: type,
+ providerId: resolvedProviderId,
+ },
+ request,
+ })
+
return NextResponse.json({ credential: created }, { status: 201 })
} catch (error: any) {
if (error?.code === '23505') {
diff --git a/apps/sim/app/api/environment/route.ts b/apps/sim/app/api/environment/route.ts
index 229ba26382f..f8167e92ac2 100644
--- a/apps/sim/app/api/environment/route.ts
+++ b/apps/sim/app/api/environment/route.ts
@@ -67,8 +67,13 @@ export async function POST(req: NextRequest) {
actorEmail: session.user.email,
action: AuditAction.ENVIRONMENT_UPDATED,
resourceType: AuditResourceType.ENVIRONMENT,
- description: 'Updated global environment variables',
- metadata: { variableCount: Object.keys(variables).length },
+ resourceId: session.user.id,
+ description: `Updated ${Object.keys(variables).length} personal environment variable(s)`,
+ metadata: {
+ variableCount: Object.keys(variables).length,
+ updatedKeys: Object.keys(variables),
+ scope: 'personal',
+ },
request: req,
})
diff --git a/apps/sim/app/api/folders/route.ts b/apps/sim/app/api/folders/route.ts
index 98e80f5aa3d..37e0ae8d1d8 100644
--- a/apps/sim/app/api/folders/route.ts
+++ b/apps/sim/app/api/folders/route.ts
@@ -168,7 +168,13 @@ export async function POST(request: NextRequest) {
resourceId: id,
resourceName: name.trim(),
description: `Created folder "${name.trim()}"`,
- metadata: { name: name.trim() },
+ metadata: {
+ name: name.trim(),
+ workspaceId,
+ parentId: parentId || undefined,
+ color: color || '#6B7280',
+ sortOrder: newFolder.sortOrder,
+ },
request,
})
diff --git a/apps/sim/app/api/form/manage/[id]/route.ts b/apps/sim/app/api/form/manage/[id]/route.ts
index 577363b8d9c..a57a7c937bb 100644
--- a/apps/sim/app/api/form/manage/[id]/route.ts
+++ b/apps/sim/app/api/form/manage/[id]/route.ts
@@ -197,8 +197,14 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
resourceId: id,
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
- resourceName: formRecord.title ?? undefined,
- description: `Updated form "${formRecord.title}"`,
+ resourceName: (title || formRecord.title) ?? undefined,
+ description: `Updated form "${title || formRecord.title}"`,
+ metadata: {
+ identifier: identifier || formRecord.identifier,
+ workflowId: formRecord.workflowId,
+ authType: authType || formRecord.authType,
+ updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'),
+ },
request,
})
@@ -255,6 +261,7 @@ export async function DELETE(
actorEmail: session.user.email ?? undefined,
resourceName: formRecord.title ?? undefined,
description: `Deleted form "${formRecord.title}"`,
+ metadata: { identifier: formRecord.identifier, workflowId: formRecord.workflowId },
request,
})
diff --git a/apps/sim/app/api/form/route.ts b/apps/sim/app/api/form/route.ts
index 6512ba95808..db29c2759de 100644
--- a/apps/sim/app/api/form/route.ts
+++ b/apps/sim/app/api/form/route.ts
@@ -208,6 +208,7 @@ export async function POST(request: NextRequest) {
actorEmail: session.user.email ?? undefined,
resourceName: title,
description: `Created form "${title}" for workflow ${workflowId}`,
+ metadata: { identifier, workflowId, authType, formUrl, showBranding },
request,
})
diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts
index 48e0d0deb2d..c5e7878fc69 100644
--- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts
@@ -194,7 +194,13 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
resourceType: AuditResourceType.CONNECTOR,
resourceId: connectorId,
description: `Restored ${updated.length} excluded document(s) for knowledge base "${writeCheck.knowledgeBase.name}"`,
- metadata: { knowledgeBaseId, documentCount: updated.length },
+ metadata: {
+ knowledgeBaseId,
+ knowledgeBaseName: writeCheck.knowledgeBase.name,
+ operation: 'restore',
+ documentCount: updated.length,
+ documentIds: updated.map((d) => d.id),
+ },
request,
})
@@ -229,7 +235,13 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
resourceType: AuditResourceType.CONNECTOR,
resourceId: connectorId,
description: `Excluded ${updated.length} document(s) from knowledge base "${writeCheck.knowledgeBase.name}"`,
- metadata: { knowledgeBaseId, documentCount: updated.length },
+ metadata: {
+ knowledgeBaseId,
+ knowledgeBaseName: writeCheck.knowledgeBase.name,
+ operation: 'exclude',
+ documentCount: updated.length,
+ documentIds: updated.map((d) => d.id),
+ },
request,
})
diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts
index 87cdb51a737..6ffee2355a1 100644
--- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts
@@ -268,7 +268,16 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
resourceId: connectorId,
resourceName: updatedData.connectorType,
description: `Updated connector for knowledge base "${writeCheck.knowledgeBase.name}"`,
- metadata: { knowledgeBaseId, updatedFields: Object.keys(parsed.data) },
+ metadata: {
+ knowledgeBaseId,
+ knowledgeBaseName: writeCheck.knowledgeBase.name,
+ connectorType: updatedData.connectorType,
+ updatedFields: Object.keys(parsed.data),
+ ...(parsed.data.syncIntervalMinutes !== undefined && {
+ syncIntervalMinutes: parsed.data.syncIntervalMinutes,
+ }),
+ ...(parsed.data.status !== undefined && { newStatus: parsed.data.status }),
+ },
request,
})
@@ -399,6 +408,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
description: `Deleted connector from knowledge base "${writeCheck.knowledgeBase.name}"`,
metadata: {
knowledgeBaseId,
+ knowledgeBaseName: writeCheck.knowledgeBase.name,
+ connectorType: existingConnector[0].connectorType,
+ deleteDocuments,
documentsDeleted: deleteDocuments ? docCount : 0,
documentsKept: deleteDocuments ? 0 : docCount,
},
diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts
index df7057fc904..1ace24c886b 100644
--- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts
@@ -78,7 +78,13 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
resourceId: connectorId,
resourceName: connectorRows[0].connectorType,
description: `Triggered manual sync for connector on knowledge base "${writeCheck.knowledgeBase.name}"`,
- metadata: { knowledgeBaseId },
+ metadata: {
+ knowledgeBaseId,
+ knowledgeBaseName: writeCheck.knowledgeBase.name,
+ connectorType: connectorRows[0].connectorType,
+ connectorStatus: connectorRows[0].status,
+ syncType: 'manual',
+ },
request,
})
diff --git a/apps/sim/app/api/knowledge/[id]/connectors/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/route.ts
index b5e2cb86f46..34da8e03276 100644
--- a/apps/sim/app/api/knowledge/[id]/connectors/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/connectors/route.ts
@@ -286,7 +286,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
resourceId: connectorId,
resourceName: connectorType,
description: `Created ${connectorType} connector for knowledge base "${writeCheck.knowledgeBase.name}"`,
- metadata: { knowledgeBaseId, connectorType, syncIntervalMinutes },
+ metadata: {
+ knowledgeBaseId,
+ knowledgeBaseName: writeCheck.knowledgeBase.name,
+ connectorType,
+ syncIntervalMinutes,
+ authMode: connectorConfig.auth.mode,
+ },
request,
})
diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts
index 4f8735826b1..f238ac4f978 100644
--- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts
@@ -208,7 +208,16 @@ export async function PUT(
resourceType: AuditResourceType.DOCUMENT,
resourceId: documentId,
resourceName: validatedData.filename ?? accessCheck.document?.filename,
- description: `Updated document "${documentId}" in knowledge base "${knowledgeBaseId}"`,
+ description: `Updated document "${validatedData.filename ?? accessCheck.document?.filename}" in knowledge base "${knowledgeBaseId}"`,
+ metadata: {
+ knowledgeBaseId,
+ knowledgeBaseName: accessCheck.knowledgeBase?.name,
+ fileName: validatedData.filename ?? accessCheck.document?.filename,
+ updatedFields: Object.keys(validatedData).filter(
+ (k) => validatedData[k as keyof typeof validatedData] !== undefined
+ ),
+ ...(validatedData.enabled !== undefined && { enabled: validatedData.enabled }),
+ },
request: req,
})
@@ -281,8 +290,14 @@ export async function DELETE(
resourceType: AuditResourceType.DOCUMENT,
resourceId: documentId,
resourceName: accessCheck.document?.filename,
- description: `Deleted document "${documentId}" from knowledge base "${knowledgeBaseId}"`,
- metadata: { fileName: accessCheck.document?.filename },
+ description: `Deleted document "${accessCheck.document?.filename}" from knowledge base "${knowledgeBaseId}"`,
+ metadata: {
+ knowledgeBaseId,
+ knowledgeBaseName: accessCheck.knowledgeBase?.name,
+ fileName: accessCheck.document?.filename,
+ fileSize: accessCheck.document?.fileSize,
+ mimeType: accessCheck.document?.mimeType,
+ },
request: req,
})
diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/documents/route.ts
index 83056e8f486..b5614aec41d 100644
--- a/apps/sim/app/api/knowledge/[id]/documents/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts
@@ -278,8 +278,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
resourceName: `${createdDocuments.length} document(s)`,
description: `Uploaded ${createdDocuments.length} document(s) to knowledge base "${knowledgeBaseId}"`,
metadata: {
+ knowledgeBaseName: accessCheck.knowledgeBase?.name,
fileCount: createdDocuments.length,
- fileNames: createdDocuments.map((doc) => doc.filename),
},
request: req,
})
@@ -358,6 +358,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
resourceName: validatedData.filename,
description: `Uploaded document "${validatedData.filename}" to knowledge base "${knowledgeBaseId}"`,
metadata: {
+ knowledgeBaseName: accessCheck.knowledgeBase?.name,
fileName: validatedData.filename,
fileType: validatedData.mimeType,
fileSize: validatedData.fileSize,
diff --git a/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts b/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts
index 59be57cd610..8d5ee153918 100644
--- a/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts
@@ -196,7 +196,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
? `Upserted (replaced) document "${validatedData.filename}" in knowledge base "${knowledgeBaseId}"`
: `Upserted (created) document "${validatedData.filename}" in knowledge base "${knowledgeBaseId}"`,
metadata: {
+ knowledgeBaseName: accessCheck.knowledgeBase?.name,
fileName: validatedData.filename,
+ fileType: validatedData.mimeType,
+ fileSize: validatedData.fileSize,
previousDocumentId: existingDocumentId,
isUpdate,
},
diff --git a/apps/sim/app/api/knowledge/[id]/restore/route.ts b/apps/sim/app/api/knowledge/[id]/restore/route.ts
index 1d37f664ab7..02d8b3e5afd 100644
--- a/apps/sim/app/api/knowledge/[id]/restore/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/restore/route.ts
@@ -59,6 +59,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
resourceId: id,
resourceName: kb.name,
description: `Restored knowledge base "${kb.name}"`,
+ metadata: {
+ knowledgeBaseName: kb.name,
+ },
request,
})
diff --git a/apps/sim/app/api/knowledge/[id]/route.ts b/apps/sim/app/api/knowledge/[id]/route.ts
index 2dcf53701da..5da7026a454 100644
--- a/apps/sim/app/api/knowledge/[id]/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/route.ts
@@ -147,6 +147,20 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
resourceId: id,
resourceName: validatedData.name ?? updatedKnowledgeBase.name,
description: `Updated knowledge base "${validatedData.name ?? updatedKnowledgeBase.name}"`,
+ metadata: {
+ updatedFields: Object.keys(validatedData).filter(
+ (k) => validatedData[k as keyof typeof validatedData] !== undefined
+ ),
+ ...(validatedData.name && { newName: validatedData.name }),
+ ...(validatedData.description !== undefined && {
+ description: validatedData.description,
+ }),
+ ...(validatedData.chunkingConfig && {
+ chunkMaxSize: validatedData.chunkingConfig.maxSize,
+ chunkMinSize: validatedData.chunkingConfig.minSize,
+ chunkOverlap: validatedData.chunkingConfig.overlap,
+ }),
+ },
request: req,
})
@@ -226,6 +240,9 @@ export async function DELETE(
resourceId: id,
resourceName: accessCheck.knowledgeBase.name,
description: `Deleted knowledge base "${accessCheck.knowledgeBase.name || id}"`,
+ metadata: {
+ knowledgeBaseName: accessCheck.knowledgeBase.name,
+ },
request: _request,
})
diff --git a/apps/sim/app/api/knowledge/route.ts b/apps/sim/app/api/knowledge/route.ts
index 20499ce8fce..9641f3a4539 100644
--- a/apps/sim/app/api/knowledge/route.ts
+++ b/apps/sim/app/api/knowledge/route.ts
@@ -162,7 +162,16 @@ export async function POST(req: NextRequest) {
resourceId: newKnowledgeBase.id,
resourceName: validatedData.name,
description: `Created knowledge base "${validatedData.name}"`,
- metadata: { name: validatedData.name },
+ metadata: {
+ name: validatedData.name,
+ description: validatedData.description,
+ embeddingModel: validatedData.embeddingModel,
+ embeddingDimension: validatedData.embeddingDimension,
+ chunkingStrategy: validatedData.chunkingConfig.strategy,
+ chunkMaxSize: validatedData.chunkingConfig.maxSize,
+ chunkMinSize: validatedData.chunkingConfig.minSize,
+ chunkOverlap: validatedData.chunkingConfig.overlap,
+ },
request: req,
})
diff --git a/apps/sim/app/api/mcp/servers/[id]/route.ts b/apps/sim/app/api/mcp/servers/[id]/route.ts
index 54265bb687c..67c893fc754 100644
--- a/apps/sim/app/api/mcp/servers/[id]/route.ts
+++ b/apps/sim/app/api/mcp/servers/[id]/route.ts
@@ -124,6 +124,14 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
resourceId: serverId,
resourceName: updatedServer.name || serverId,
description: `Updated MCP server "${updatedServer.name || serverId}"`,
+ metadata: {
+ serverName: updatedServer.name,
+ transport: updatedServer.transport,
+ url: updatedServer.url,
+ updatedFields: Object.keys(updateData).filter(
+ (k) => k !== 'workspaceId' && k !== 'updatedAt'
+ ),
+ },
request,
})
diff --git a/apps/sim/app/api/mcp/servers/route.ts b/apps/sim/app/api/mcp/servers/route.ts
index 054c7a3a2ca..5d5c1d8b6fe 100644
--- a/apps/sim/app/api/mcp/servers/route.ts
+++ b/apps/sim/app/api/mcp/servers/route.ts
@@ -206,7 +206,14 @@ export const POST = withMcpAuth('write')(
resourceId: serverId,
resourceName: body.name,
description: `Added MCP server "${body.name}"`,
- metadata: { serverName: body.name, transport: body.transport },
+ metadata: {
+ serverName: body.name,
+ transport: body.transport,
+ url: body.url,
+ timeout: body.timeout || 30000,
+ retries: body.retries || 3,
+ source: source,
+ },
request,
})
@@ -278,6 +285,12 @@ export const DELETE = withMcpAuth('admin')(
resourceId: serverId!,
resourceName: deletedServer.name,
description: `Removed MCP server "${deletedServer.name}"`,
+ metadata: {
+ serverName: deletedServer.name,
+ transport: deletedServer.transport,
+ url: deletedServer.url,
+ source,
+ },
request,
})
diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts
index f5ed5371e19..6ed1bb2e0c6 100644
--- a/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts
+++ b/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts
@@ -135,6 +135,11 @@ export const PATCH = withMcpAuth('write')(
resourceId: serverId,
resourceName: updatedServer.name,
description: `Updated workflow MCP server "${updatedServer.name}"`,
+ metadata: {
+ serverName: updatedServer.name,
+ isPublic: updatedServer.isPublic,
+ updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'),
+ },
request,
})
@@ -189,6 +194,7 @@ export const DELETE = withMcpAuth('admin')(
resourceId: serverId,
resourceName: deletedServer.name,
description: `Unpublished workflow MCP server "${deletedServer.name}"`,
+ metadata: { serverName: deletedServer.name },
request,
})
diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts
index f54caf4703e..60791a36bcf 100644
--- a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts
+++ b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts
@@ -152,7 +152,12 @@ export const PATCH = withMcpAuth('write')(
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
description: `Updated tool "${updatedTool.toolName}" in MCP server`,
- metadata: { toolId, toolName: updatedTool.toolName },
+ metadata: {
+ toolId,
+ toolName: updatedTool.toolName,
+ workflowId: updatedTool.workflowId,
+ updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'),
+ },
request,
})
@@ -220,7 +225,7 @@ export const DELETE = withMcpAuth('write')(
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
description: `Removed tool "${deletedTool.toolName}" from MCP server`,
- metadata: { toolId, toolName: deletedTool.toolName },
+ metadata: { toolId, toolName: deletedTool.toolName, workflowId: deletedTool.workflowId },
request,
})
diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts
index 1a4687b44fc..396cfe92468 100644
--- a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts
+++ b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts
@@ -224,7 +224,13 @@ export const POST = withMcpAuth('write')(
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
description: `Added tool "${toolName}" to MCP server`,
- metadata: { toolId, toolName, workflowId: body.workflowId },
+ metadata: {
+ toolId,
+ toolName,
+ toolDescription,
+ workflowId: body.workflowId,
+ workflowName: workflowRecord.name,
+ },
request,
})
diff --git a/apps/sim/app/api/mcp/workflow-servers/route.ts b/apps/sim/app/api/mcp/workflow-servers/route.ts
index 84d431fa423..807df769673 100644
--- a/apps/sim/app/api/mcp/workflow-servers/route.ts
+++ b/apps/sim/app/api/mcp/workflow-servers/route.ts
@@ -208,6 +208,13 @@ export const POST = withMcpAuth('write')(
resourceId: serverId,
resourceName: body.name.trim(),
description: `Published workflow MCP server "${body.name.trim()}" with ${addedTools.length} tool(s)`,
+ metadata: {
+ serverName: body.name.trim(),
+ isPublic: body.isPublic ?? false,
+ toolCount: addedTools.length,
+ toolNames: addedTools.map((t) => t.toolName),
+ workflowIds: addedTools.map((t) => t.workflowId),
+ },
request,
})
diff --git a/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts b/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts
index f54e72b2701..7f4f7d8004c 100644
--- a/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts
+++ b/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts
@@ -182,6 +182,20 @@ export async function POST(
email: orgInvitation.email,
})
+ recordAudit({
+ workspaceId: null,
+ actorId: session.user.id,
+ action: AuditAction.ORG_INVITATION_RESENT,
+ resourceType: AuditResourceType.ORGANIZATION,
+ resourceId: organizationId,
+ actorName: session.user.name ?? undefined,
+ actorEmail: session.user.email ?? undefined,
+ resourceName: org?.name ?? undefined,
+ description: `Resent organization invitation to ${orgInvitation.email}`,
+ metadata: { invitationId, targetEmail: orgInvitation.email, targetRole: orgInvitation.role },
+ request: _request,
+ })
+
return NextResponse.json({
success: true,
message: 'Invitation resent successfully',
diff --git a/apps/sim/app/api/organizations/[id]/invitations/route.ts b/apps/sim/app/api/organizations/[id]/invitations/route.ts
index 001184d98e7..5a85cfbb4f0 100644
--- a/apps/sim/app/api/organizations/[id]/invitations/route.ts
+++ b/apps/sim/app/api/organizations/[id]/invitations/route.ts
@@ -423,7 +423,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
actorEmail: session.user.email ?? undefined,
resourceName: organizationEntry[0]?.name,
description: `Invited ${inv.email} to organization as ${role}`,
- metadata: { invitationId: inv.id, targetEmail: inv.email, targetRole: role },
+ metadata: {
+ invitationId: inv.id,
+ targetEmail: inv.email,
+ targetRole: role,
+ isBatch,
+ workspaceInvitationCount: validWorkspaceInvitations.length,
+ },
request,
})
}
@@ -558,7 +564,7 @@ export async function DELETE(
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
description: `Revoked organization invitation for ${result[0].email}`,
- metadata: { invitationId, targetEmail: result[0].email },
+ metadata: { invitationId, targetEmail: result[0].email, targetRole: result[0].role },
request,
})
diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts
index 3b15d34848e..989d792b6fd 100644
--- a/apps/sim/app/api/organizations/[id]/members/route.ts
+++ b/apps/sim/app/api/organizations/[id]/members/route.ts
@@ -294,6 +294,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
resourceId: organizationId,
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
+ resourceName: organizationEntry[0]?.name ?? undefined,
description: `Invited ${normalizedEmail} to organization as ${role}`,
metadata: { invitationId, targetEmail: normalizedEmail, targetRole: role },
request,
diff --git a/apps/sim/app/api/organizations/route.ts b/apps/sim/app/api/organizations/route.ts
index 5803f85dc25..6185f120f46 100644
--- a/apps/sim/app/api/organizations/route.ts
+++ b/apps/sim/app/api/organizations/route.ts
@@ -126,6 +126,7 @@ export async function POST(request: Request) {
actorEmail: user.email ?? undefined,
resourceName: organizationName ?? undefined,
description: `Created organization "${organizationName}"`,
+ metadata: { organizationSlug },
request,
})
diff --git a/apps/sim/app/api/permission-groups/[id]/route.ts b/apps/sim/app/api/permission-groups/[id]/route.ts
index 9391b67d826..51cbe1222b6 100644
--- a/apps/sim/app/api/permission-groups/[id]/route.ts
+++ b/apps/sim/app/api/permission-groups/[id]/route.ts
@@ -193,6 +193,12 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
actorEmail: session.user.email ?? undefined,
resourceName: updated.name,
description: `Updated permission group "${updated.name}"`,
+ metadata: {
+ organizationId: result.group.organizationId,
+ updatedFields: Object.keys(updates).filter(
+ (k) => updates[k as keyof typeof updates] !== undefined
+ ),
+ },
request: req,
})
@@ -254,6 +260,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
actorEmail: session.user.email ?? undefined,
resourceName: result.group.name,
description: `Deleted permission group "${result.group.name}"`,
+ metadata: { organizationId: result.group.organizationId },
request: req,
})
diff --git a/apps/sim/app/api/permission-groups/route.ts b/apps/sim/app/api/permission-groups/route.ts
index b79f01ebc5f..9b88d482617 100644
--- a/apps/sim/app/api/permission-groups/route.ts
+++ b/apps/sim/app/api/permission-groups/route.ts
@@ -211,6 +211,7 @@ export async function POST(req: Request) {
actorEmail: session.user.email ?? undefined,
resourceName: name,
description: `Created permission group "${name}"`,
+ metadata: { organizationId, autoAddNewMembers: autoAddNewMembers || false },
request: req,
})
diff --git a/apps/sim/app/api/schedules/[id]/route.ts b/apps/sim/app/api/schedules/[id]/route.ts
index 597634aeb9d..d05514a8837 100644
--- a/apps/sim/app/api/schedules/[id]/route.ts
+++ b/apps/sim/app/api/schedules/[id]/route.ts
@@ -38,6 +38,7 @@ type ScheduleRow = {
timezone: string | null
sourceType: string | null
sourceWorkspaceId: string | null
+ jobTitle: string | null
}
async function fetchAndAuthorize(
@@ -55,6 +56,7 @@ async function fetchAndAuthorize(
timezone: workflowSchedule.timezone,
sourceType: workflowSchedule.sourceType,
sourceWorkspaceId: workflowSchedule.sourceWorkspaceId,
+ jobTitle: workflowSchedule.jobTitle,
})
.from(workflowSchedule)
.where(and(eq(workflowSchedule.id, scheduleId), isNull(workflowSchedule.archivedAt)))
@@ -144,13 +146,18 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
recordAudit({
workspaceId,
actorId: session.user.id,
+ actorName: session.user.name,
+ actorEmail: session.user.email,
action: AuditAction.SCHEDULE_UPDATED,
resourceType: AuditResourceType.SCHEDULE,
resourceId: scheduleId,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- description: `Disabled schedule ${scheduleId}`,
- metadata: {},
+ resourceName: schedule.jobTitle ?? undefined,
+ description: `Disabled schedule "${schedule.jobTitle ?? scheduleId}"`,
+ metadata: {
+ operation: 'disable',
+ sourceType: schedule.sourceType,
+ previousStatus: schedule.status,
+ },
request,
})
@@ -204,13 +211,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
recordAudit({
workspaceId,
actorId: session.user.id,
+ actorName: session.user.name,
+ actorEmail: session.user.email,
action: AuditAction.SCHEDULE_UPDATED,
resourceType: AuditResourceType.SCHEDULE,
resourceId: scheduleId,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- description: `Updated job schedule ${scheduleId}`,
- metadata: {},
+ resourceName: schedule.jobTitle ?? undefined,
+ description: `Updated job schedule "${schedule.jobTitle ?? scheduleId}"`,
+ metadata: {
+ operation: 'update',
+ updatedFields: Object.keys(setFields).filter((k) => k !== 'updatedAt'),
+ },
request,
})
@@ -246,13 +257,19 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
recordAudit({
workspaceId,
actorId: session.user.id,
+ actorName: session.user.name,
+ actorEmail: session.user.email,
action: AuditAction.SCHEDULE_UPDATED,
resourceType: AuditResourceType.SCHEDULE,
resourceId: scheduleId,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- description: `Reactivated schedule ${scheduleId}`,
- metadata: { cronExpression: schedule.cronExpression, timezone: schedule.timezone },
+ resourceName: schedule.jobTitle ?? undefined,
+ description: `Reactivated schedule "${schedule.jobTitle ?? scheduleId}"`,
+ metadata: {
+ operation: 'reactivate',
+ sourceType: schedule.sourceType,
+ cronExpression: schedule.cronExpression,
+ timezone: schedule.timezone,
+ },
request,
})
@@ -289,13 +306,18 @@ export async function DELETE(
recordAudit({
workspaceId,
actorId: session.user.id,
- action: AuditAction.SCHEDULE_UPDATED,
+ actorName: session.user.name,
+ actorEmail: session.user.email,
+ action: AuditAction.SCHEDULE_DELETED,
resourceType: AuditResourceType.SCHEDULE,
resourceId: scheduleId,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- description: `Deleted ${schedule.sourceType === 'job' ? 'job' : 'schedule'} ${scheduleId}`,
- metadata: {},
+ resourceName: schedule.jobTitle ?? undefined,
+ description: `Deleted ${schedule.sourceType === 'job' ? 'job' : 'schedule'} "${schedule.jobTitle ?? scheduleId}"`,
+ metadata: {
+ sourceType: schedule.sourceType,
+ cronExpression: schedule.cronExpression,
+ timezone: schedule.timezone,
+ },
request,
})
diff --git a/apps/sim/app/api/schedules/route.ts b/apps/sim/app/api/schedules/route.ts
index da291cdcccc..a2f14f109a8 100644
--- a/apps/sim/app/api/schedules/route.ts
+++ b/apps/sim/app/api/schedules/route.ts
@@ -3,6 +3,7 @@ import { workflow, workflowDeploymentVersion, workflowSchedule } from '@sim/db/s
import { createLogger } from '@sim/logger'
import { and, eq, isNull, or } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
+import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
@@ -279,6 +280,25 @@ export async function POST(req: NextRequest) {
lifecycle,
})
+ recordAudit({
+ workspaceId,
+ actorId: session.user.id,
+ actorName: session.user.name,
+ actorEmail: session.user.email,
+ action: AuditAction.SCHEDULE_CREATED,
+ resourceType: AuditResourceType.SCHEDULE,
+ resourceId: id,
+ resourceName: title.trim(),
+ description: `Created job schedule "${title.trim()}"`,
+ metadata: {
+ cronExpression,
+ timezone,
+ lifecycle,
+ maxRuns: maxRuns ?? null,
+ },
+ request: req,
+ })
+
captureServerEvent(
session.user.id,
'scheduled_task_created',
diff --git a/apps/sim/app/api/skills/route.ts b/apps/sim/app/api/skills/route.ts
index 41173c13188..f1db74a3cce 100644
--- a/apps/sim/app/api/skills/route.ts
+++ b/apps/sim/app/api/skills/route.ts
@@ -103,11 +103,14 @@ export async function POST(req: NextRequest) {
recordAudit({
workspaceId,
actorId: userId,
+ actorName: authResult.userName ?? undefined,
+ actorEmail: authResult.userEmail ?? undefined,
action: AuditAction.SKILL_CREATED,
resourceType: AuditResourceType.SKILL,
resourceId: skill.id,
resourceName: skill.name,
description: `Created/updated skill "${skill.name}"`,
+ metadata: { source },
})
captureServerEvent(
userId,
@@ -185,10 +188,13 @@ export async function DELETE(request: NextRequest) {
recordAudit({
workspaceId,
actorId: authResult.userId,
+ actorName: authResult.userName ?? undefined,
+ actorEmail: authResult.userEmail ?? undefined,
action: AuditAction.SKILL_DELETED,
resourceType: AuditResourceType.SKILL,
resourceId: skillId,
description: `Deleted skill`,
+ metadata: { source },
})
captureServerEvent(
diff --git a/apps/sim/app/api/table/[tableId]/restore/route.ts b/apps/sim/app/api/table/[tableId]/restore/route.ts
index 9175de0b661..fca864c8753 100644
--- a/apps/sim/app/api/table/[tableId]/restore/route.ts
+++ b/apps/sim/app/api/table/[tableId]/restore/route.ts
@@ -45,6 +45,10 @@ export async function POST(
resourceId: tableId,
resourceName: table.name,
description: `Restored table "${table.name}"`,
+ metadata: {
+ tableName: table.name,
+ workspaceId: table.workspaceId,
+ },
request,
})
diff --git a/apps/sim/app/api/templates/[id]/route.ts b/apps/sim/app/api/templates/[id]/route.ts
index 260b64f582b..82c73fffa0f 100644
--- a/apps/sim/app/api/templates/[id]/route.ts
+++ b/apps/sim/app/api/templates/[id]/route.ts
@@ -251,6 +251,15 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
resourceId: id,
resourceName: name ?? template.name,
description: `Updated template "${name ?? template.name}"`,
+ metadata: {
+ templateName: name ?? template.name,
+ updatedFields: Object.keys(validationResult.data).filter(
+ (k) => validationResult.data[k as keyof typeof validationResult.data] !== undefined
+ ),
+ statusChange: status !== undefined ? { from: template.status, to: status } : undefined,
+ stateUpdated: updateState || false,
+ workflowId: template.workflowId || undefined,
+ },
request,
})
@@ -317,6 +326,13 @@ export async function DELETE(
resourceId: id,
resourceName: template.name,
description: `Deleted template "${template.name}"`,
+ metadata: {
+ templateName: template.name,
+ workflowId: template.workflowId || undefined,
+ creatorId: template.creatorId || undefined,
+ status: template.status,
+ tags: template.tags,
+ },
request,
})
diff --git a/apps/sim/app/api/templates/route.ts b/apps/sim/app/api/templates/route.ts
index c6424865c82..0a9f9d02b72 100644
--- a/apps/sim/app/api/templates/route.ts
+++ b/apps/sim/app/api/templates/route.ts
@@ -346,6 +346,14 @@ export async function POST(request: NextRequest) {
resourceId: templateId,
resourceName: data.name,
description: `Created template "${data.name}"`,
+ metadata: {
+ templateName: data.name,
+ workflowId: data.workflowId,
+ creatorId: data.creatorId,
+ tags: data.tags,
+ tagline: data.details?.tagline || undefined,
+ status: 'pending',
+ },
request,
})
diff --git a/apps/sim/app/api/tools/custom/route.ts b/apps/sim/app/api/tools/custom/route.ts
index 7d45353e609..426da0273ce 100644
--- a/apps/sim/app/api/tools/custom/route.ts
+++ b/apps/sim/app/api/tools/custom/route.ts
@@ -183,11 +183,14 @@ export async function POST(req: NextRequest) {
recordAudit({
workspaceId,
actorId: userId,
+ actorName: authResult.userName ?? undefined,
+ actorEmail: authResult.userEmail ?? undefined,
action: AuditAction.CUSTOM_TOOL_CREATED,
resourceType: AuditResourceType.CUSTOM_TOOL,
resourceId: tool.id,
resourceName: tool.title,
description: `Created/updated custom tool "${tool.title}"`,
+ metadata: { source },
})
}
@@ -304,10 +307,14 @@ export async function DELETE(request: NextRequest) {
recordAudit({
workspaceId: tool.workspaceId || undefined,
actorId: userId,
+ actorName: authResult.userName ?? undefined,
+ actorEmail: authResult.userEmail ?? undefined,
action: AuditAction.CUSTOM_TOOL_DELETED,
resourceType: AuditResourceType.CUSTOM_TOOL,
resourceId: toolId,
- description: `Deleted custom tool`,
+ resourceName: tool.title,
+ description: `Deleted custom tool "${tool.title}"`,
+ metadata: { source },
})
logger.info(`[${requestId}] Deleted tool: ${toolId}`)
diff --git a/apps/sim/app/api/v1/admin/audit-logs/route.ts b/apps/sim/app/api/v1/admin/audit-logs/route.ts
index 895ac1ff3e2..f97c755da33 100644
--- a/apps/sim/app/api/v1/admin/audit-logs/route.ts
+++ b/apps/sim/app/api/v1/admin/audit-logs/route.ts
@@ -21,7 +21,7 @@
import { db } from '@sim/db'
import { auditLog } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
-import { and, count, desc, eq, gte, lte, type SQL } from 'drizzle-orm'
+import { and, count, desc } from 'drizzle-orm'
import { withAdminAuth } from '@/app/api/v1/admin/middleware'
import {
badRequestResponse,
@@ -34,6 +34,7 @@ import {
parsePaginationParams,
toAdminAuditLog,
} from '@/app/api/v1/admin/types'
+import { buildFilterConditions } from '@/app/api/v1/audit-logs/query'
const logger = createLogger('AdminAuditLogsAPI')
@@ -41,33 +42,27 @@ export const GET = withAdminAuth(async (request) => {
const url = new URL(request.url)
const { limit, offset } = parsePaginationParams(url)
- const actionFilter = url.searchParams.get('action')
- const resourceTypeFilter = url.searchParams.get('resourceType')
- const resourceIdFilter = url.searchParams.get('resourceId')
- const workspaceIdFilter = url.searchParams.get('workspaceId')
- const actorIdFilter = url.searchParams.get('actorId')
- const actorEmailFilter = url.searchParams.get('actorEmail')
- const startDateFilter = url.searchParams.get('startDate')
- const endDateFilter = url.searchParams.get('endDate')
+ const startDate = url.searchParams.get('startDate') || undefined
+ const endDate = url.searchParams.get('endDate') || undefined
- if (startDateFilter && Number.isNaN(Date.parse(startDateFilter))) {
+ if (startDate && Number.isNaN(Date.parse(startDate))) {
return badRequestResponse('Invalid startDate format. Use ISO 8601.')
}
- if (endDateFilter && Number.isNaN(Date.parse(endDateFilter))) {
+ if (endDate && Number.isNaN(Date.parse(endDate))) {
return badRequestResponse('Invalid endDate format. Use ISO 8601.')
}
try {
- const conditions: SQL[] = []
-
- if (actionFilter) conditions.push(eq(auditLog.action, actionFilter))
- if (resourceTypeFilter) conditions.push(eq(auditLog.resourceType, resourceTypeFilter))
- if (resourceIdFilter) conditions.push(eq(auditLog.resourceId, resourceIdFilter))
- if (workspaceIdFilter) conditions.push(eq(auditLog.workspaceId, workspaceIdFilter))
- if (actorIdFilter) conditions.push(eq(auditLog.actorId, actorIdFilter))
- if (actorEmailFilter) conditions.push(eq(auditLog.actorEmail, actorEmailFilter))
- if (startDateFilter) conditions.push(gte(auditLog.createdAt, new Date(startDateFilter)))
- if (endDateFilter) conditions.push(lte(auditLog.createdAt, new Date(endDateFilter)))
+ const conditions = buildFilterConditions({
+ action: url.searchParams.get('action') || undefined,
+ resourceType: url.searchParams.get('resourceType') || undefined,
+ resourceId: url.searchParams.get('resourceId') || undefined,
+ workspaceId: url.searchParams.get('workspaceId') || undefined,
+ actorId: url.searchParams.get('actorId') || undefined,
+ actorEmail: url.searchParams.get('actorEmail') || undefined,
+ startDate,
+ endDate,
+ })
const whereClause = conditions.length > 0 ? and(...conditions) : undefined
diff --git a/apps/sim/app/api/v1/audit-logs/query.ts b/apps/sim/app/api/v1/audit-logs/query.ts
new file mode 100644
index 00000000000..14e24c65427
--- /dev/null
+++ b/apps/sim/app/api/v1/audit-logs/query.ts
@@ -0,0 +1,146 @@
+import { db } from '@sim/db'
+import { auditLog, workspace } from '@sim/db/schema'
+import type { InferSelectModel } from 'drizzle-orm'
+import { and, desc, eq, gte, ilike, inArray, lt, lte, or, type SQL, sql } from 'drizzle-orm'
+
+type DbAuditLog = InferSelectModel
+
+interface CursorData {
+ createdAt: string
+ id: string
+}
+
+function encodeCursor(data: CursorData): string {
+ return Buffer.from(JSON.stringify(data)).toString('base64')
+}
+
+function decodeCursor(cursor: string): CursorData | null {
+ try {
+ return JSON.parse(Buffer.from(cursor, 'base64').toString())
+ } catch {
+ return null
+ }
+}
+
+export interface AuditLogFilterParams {
+ action?: string
+ resourceType?: string
+ resourceId?: string
+ workspaceId?: string
+ actorId?: string
+ actorEmail?: string
+ search?: string
+ startDate?: string
+ endDate?: string
+}
+
+export function buildFilterConditions(params: AuditLogFilterParams): SQL[] {
+ const conditions: SQL[] = []
+
+ if (params.action) conditions.push(eq(auditLog.action, params.action))
+ if (params.resourceType) conditions.push(eq(auditLog.resourceType, params.resourceType))
+ if (params.resourceId) conditions.push(eq(auditLog.resourceId, params.resourceId))
+ if (params.workspaceId) conditions.push(eq(auditLog.workspaceId, params.workspaceId))
+ if (params.actorId) conditions.push(eq(auditLog.actorId, params.actorId))
+ if (params.actorEmail) conditions.push(eq(auditLog.actorEmail, params.actorEmail))
+
+ if (params.search) {
+ const escaped = params.search.replace(/[%_\\]/g, '\\$&')
+ const searchTerm = `%${escaped}%`
+ conditions.push(
+ or(
+ ilike(auditLog.action, searchTerm),
+ ilike(auditLog.actorEmail, searchTerm),
+ ilike(auditLog.actorName, searchTerm),
+ ilike(auditLog.resourceName, searchTerm),
+ ilike(auditLog.description, searchTerm)
+ )!
+ )
+ }
+
+ if (params.startDate) conditions.push(gte(auditLog.createdAt, new Date(params.startDate)))
+ if (params.endDate) conditions.push(lte(auditLog.createdAt, new Date(params.endDate)))
+
+ return conditions
+}
+
+export async function buildOrgScopeCondition(
+ orgMemberIds: string[],
+ includeDeparted: boolean
+): Promise> {
+ if (orgMemberIds.length === 0) {
+ return sql`1 = 0`
+ }
+
+ if (!includeDeparted) {
+ return inArray(auditLog.actorId, orgMemberIds)
+ }
+
+ const orgWorkspaces = await db
+ .select({ id: workspace.id })
+ .from(workspace)
+ .where(inArray(workspace.ownerId, orgMemberIds))
+
+ const orgWorkspaceIds = orgWorkspaces.map((w) => w.id)
+
+ if (orgWorkspaceIds.length > 0) {
+ return or(
+ inArray(auditLog.actorId, orgMemberIds),
+ inArray(auditLog.workspaceId, orgWorkspaceIds)
+ )!
+ }
+
+ return inArray(auditLog.actorId, orgMemberIds)
+}
+
+function buildCursorCondition(cursor: string): SQL | null {
+ const cursorData = decodeCursor(cursor)
+ if (!cursorData?.createdAt || !cursorData.id) return null
+
+ const cursorDate = new Date(cursorData.createdAt)
+ if (Number.isNaN(cursorDate.getTime())) return null
+
+ return or(
+ lt(auditLog.createdAt, cursorDate),
+ and(eq(auditLog.createdAt, cursorDate), lt(auditLog.id, cursorData.id))
+ )!
+}
+
+interface CursorPaginatedResult {
+ data: DbAuditLog[]
+ nextCursor?: string
+}
+
+export async function queryAuditLogs(
+ conditions: SQL[],
+ limit: number,
+ cursor?: string
+): Promise {
+ const allConditions = [...conditions]
+
+ if (cursor) {
+ const cursorCondition = buildCursorCondition(cursor)
+ if (cursorCondition) allConditions.push(cursorCondition)
+ }
+
+ const rows = await db
+ .select()
+ .from(auditLog)
+ .where(allConditions.length > 0 ? and(...allConditions) : undefined)
+ .orderBy(desc(auditLog.createdAt), desc(auditLog.id))
+ .limit(limit + 1)
+
+ const hasMore = rows.length > limit
+ const data = rows.slice(0, limit)
+
+ let nextCursor: string | undefined
+ if (hasMore && data.length > 0) {
+ const last = data[data.length - 1]
+ nextCursor = encodeCursor({
+ createdAt: last.createdAt.toISOString(),
+ id: last.id,
+ })
+ }
+
+ return { data, nextCursor }
+}
diff --git a/apps/sim/app/api/v1/audit-logs/route.ts b/apps/sim/app/api/v1/audit-logs/route.ts
index 5a090391da4..046680bde44 100644
--- a/apps/sim/app/api/v1/audit-logs/route.ts
+++ b/apps/sim/app/api/v1/audit-logs/route.ts
@@ -19,15 +19,17 @@
* Response: { data: AuditLogEntry[], nextCursor?: string, limits: UserLimits }
*/
-import { db } from '@sim/db'
-import { auditLog, workspace } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
-import { and, desc, eq, gte, inArray, lt, lte, or, type SQL } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { generateId } from '@/lib/core/utils/uuid'
import { validateEnterpriseAuditAccess } from '@/app/api/v1/audit-logs/auth'
import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format'
+import {
+ buildFilterConditions,
+ buildOrgScopeCondition,
+ queryAuditLogs,
+} from '@/app/api/v1/audit-logs/query'
import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta'
import { checkRateLimit, createRateLimitResponse } from '@/app/api/v1/middleware'
@@ -57,23 +59,6 @@ const QueryParamsSchema = z.object({
cursor: z.string().optional(),
})
-interface CursorData {
- createdAt: string
- id: string
-}
-
-function encodeCursor(data: CursorData): string {
- return Buffer.from(JSON.stringify(data)).toString('base64')
-}
-
-function decodeCursor(cursor: string): CursorData | null {
- try {
- return JSON.parse(Buffer.from(cursor, 'base64').toString())
- } catch {
- return null
- }
-}
-
export async function GET(request: NextRequest) {
const requestId = generateId().slice(0, 8)
@@ -112,71 +97,22 @@ export async function GET(request: NextRequest) {
)
}
- let scopeCondition: SQL
-
- if (params.includeDeparted) {
- const orgWorkspaces = await db
- .select({ id: workspace.id })
- .from(workspace)
- .where(inArray(workspace.ownerId, orgMemberIds))
-
- const orgWorkspaceIds = orgWorkspaces.map((w) => w.id)
-
- if (orgWorkspaceIds.length > 0) {
- scopeCondition = or(
- inArray(auditLog.actorId, orgMemberIds),
- inArray(auditLog.workspaceId, orgWorkspaceIds)
- )!
- } else {
- scopeCondition = inArray(auditLog.actorId, orgMemberIds)
- }
- } else {
- scopeCondition = inArray(auditLog.actorId, orgMemberIds)
- }
-
- const conditions: SQL[] = [scopeCondition]
-
- if (params.action) conditions.push(eq(auditLog.action, params.action))
- if (params.resourceType) conditions.push(eq(auditLog.resourceType, params.resourceType))
- if (params.resourceId) conditions.push(eq(auditLog.resourceId, params.resourceId))
- if (params.workspaceId) conditions.push(eq(auditLog.workspaceId, params.workspaceId))
- if (params.actorId) conditions.push(eq(auditLog.actorId, params.actorId))
- if (params.startDate) conditions.push(gte(auditLog.createdAt, new Date(params.startDate)))
- if (params.endDate) conditions.push(lte(auditLog.createdAt, new Date(params.endDate)))
-
- if (params.cursor) {
- const cursorData = decodeCursor(params.cursor)
- if (cursorData?.createdAt && cursorData.id) {
- const cursorDate = new Date(cursorData.createdAt)
- if (!Number.isNaN(cursorDate.getTime())) {
- conditions.push(
- or(
- lt(auditLog.createdAt, cursorDate),
- and(eq(auditLog.createdAt, cursorDate), lt(auditLog.id, cursorData.id))
- )!
- )
- }
- }
- }
-
- const rows = await db
- .select()
- .from(auditLog)
- .where(and(...conditions))
- .orderBy(desc(auditLog.createdAt), desc(auditLog.id))
- .limit(params.limit + 1)
-
- const hasMore = rows.length > params.limit
- const data = rows.slice(0, params.limit)
-
- let nextCursor: string | undefined
- if (hasMore && data.length > 0) {
- const last = data[data.length - 1]
- nextCursor = encodeCursor({
- createdAt: last.createdAt.toISOString(),
- id: last.id,
- })
- }
+ const scopeCondition = await buildOrgScopeCondition(orgMemberIds, params.includeDeparted)
+ const filterConditions = buildFilterConditions({
+ action: params.action,
+ resourceType: params.resourceType,
+ resourceId: params.resourceId,
+ workspaceId: params.workspaceId,
+ actorId: params.actorId,
+ startDate: params.startDate,
+ endDate: params.endDate,
+ })
+
+ const { data, nextCursor } = await queryAuditLogs(
+ [scopeCondition, ...filterConditions],
+ params.limit,
+ params.cursor
+ )
const formattedLogs = data.map(formatAuditLogEntry)
diff --git a/apps/sim/app/api/v1/files/[fileId]/route.ts b/apps/sim/app/api/v1/files/[fileId]/route.ts
index 7007053681b..b3d3db8ceb8 100644
--- a/apps/sim/app/api/v1/files/[fileId]/route.ts
+++ b/apps/sim/app/api/v1/files/[fileId]/route.ts
@@ -142,6 +142,7 @@ export async function DELETE(request: NextRequest, { params }: FileRouteParams)
resourceId: fileId,
resourceName: fileRecord.name,
description: `Archived file "${fileRecord.name}" via API`,
+ metadata: { fileSize: fileRecord.size, fileType: fileRecord.type },
request,
})
diff --git a/apps/sim/app/api/v1/files/route.ts b/apps/sim/app/api/v1/files/route.ts
index 8c344c1575d..2781fe164a2 100644
--- a/apps/sim/app/api/v1/files/route.ts
+++ b/apps/sim/app/api/v1/files/route.ts
@@ -155,6 +155,7 @@ export async function POST(request: NextRequest) {
resourceId: userFile.id,
resourceName: file.name,
description: `Uploaded file "${file.name}" via API`,
+ metadata: { fileSize: file.size, fileType: file.type || 'application/octet-stream' },
request,
})
diff --git a/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts b/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts
index b69721329a4..22d40d979f1 100644
--- a/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts
+++ b/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts
@@ -167,6 +167,7 @@ export async function DELETE(request: NextRequest, { params }: DocumentDetailRou
resourceId: documentId,
resourceName: docs[0].filename,
description: `Deleted document "${docs[0].filename}" from knowledge base via API`,
+ metadata: { knowledgeBaseId },
request,
})
diff --git a/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts b/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts
index 7310a4eca98..6eb61e22614 100644
--- a/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts
+++ b/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts
@@ -207,6 +207,7 @@ export async function POST(request: NextRequest, { params }: DocumentsRouteParam
resourceId: newDocument.id,
resourceName: file.name,
description: `Uploaded document "${file.name}" to knowledge base via API`,
+ metadata: { knowledgeBaseId, fileSize: file.size, mimeType: contentType },
request,
})
diff --git a/apps/sim/app/api/v1/knowledge/[id]/route.ts b/apps/sim/app/api/v1/knowledge/[id]/route.ts
index 0b7012c8770..b6b5ed3c8ac 100644
--- a/apps/sim/app/api/v1/knowledge/[id]/route.ts
+++ b/apps/sim/app/api/v1/knowledge/[id]/route.ts
@@ -111,6 +111,7 @@ export async function PUT(request: NextRequest, { params }: KnowledgeRouteParams
resourceId: id,
resourceName: updatedKb.name,
description: `Updated knowledge base "${updatedKb.name}" via API`,
+ metadata: { updatedFields: Object.keys(updates) },
request,
})
diff --git a/apps/sim/app/api/v1/knowledge/route.ts b/apps/sim/app/api/v1/knowledge/route.ts
index 9d45e677bd3..61741d3c59e 100644
--- a/apps/sim/app/api/v1/knowledge/route.ts
+++ b/apps/sim/app/api/v1/knowledge/route.ts
@@ -106,6 +106,7 @@ export async function POST(request: NextRequest) {
resourceId: kb.id,
resourceName: kb.name,
description: `Created knowledge base "${kb.name}" via API`,
+ metadata: { chunkingConfig },
request,
})
diff --git a/apps/sim/app/api/v1/tables/route.ts b/apps/sim/app/api/v1/tables/route.ts
index 09ff717f9cd..d0c0ad3e64d 100644
--- a/apps/sim/app/api/v1/tables/route.ts
+++ b/apps/sim/app/api/v1/tables/route.ts
@@ -206,6 +206,7 @@ export async function POST(request: NextRequest) {
resourceId: table.id,
resourceName: table.name,
description: `Created table "${table.name}" via API`,
+ metadata: { columnCount: params.schema.columns.length },
request,
})
diff --git a/apps/sim/app/api/webhooks/[id]/route.ts b/apps/sim/app/api/webhooks/[id]/route.ts
index 24d93fc0609..e146c939507 100644
--- a/apps/sim/app/api/webhooks/[id]/route.ts
+++ b/apps/sim/app/api/webhooks/[id]/route.ts
@@ -270,8 +270,14 @@ export async function DELETE(
resourceType: AuditResourceType.WEBHOOK,
resourceId: id,
resourceName: foundWebhook.provider || 'generic',
- description: 'Deleted webhook',
- metadata: { workflowId: webhookData.workflow.id },
+ description: `Deleted ${foundWebhook.provider || 'generic'} webhook`,
+ metadata: {
+ provider: foundWebhook.provider || 'generic',
+ workflowId: webhookData.workflow.id,
+ webhookPath: foundWebhook.path || undefined,
+ blockId: foundWebhook.blockId || undefined,
+ credentialSetId: credentialSetId || undefined,
+ },
request,
})
diff --git a/apps/sim/app/api/webhooks/route.ts b/apps/sim/app/api/webhooks/route.ts
index c6ef9e992e1..0c7174f4294 100644
--- a/apps/sim/app/api/webhooks/route.ts
+++ b/apps/sim/app/api/webhooks/route.ts
@@ -687,7 +687,12 @@ export async function POST(request: NextRequest) {
resourceId: savedWebhook.id,
resourceName: provider || 'generic',
description: `Created ${provider || 'generic'} webhook`,
- metadata: { provider, workflowId },
+ metadata: {
+ provider: provider || 'generic',
+ workflowId,
+ webhookPath: finalPath,
+ blockId: blockId || undefined,
+ },
request,
})
diff --git a/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts b/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts
index a209db29eb4..a2fb4fe4ba4 100644
--- a/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts
+++ b/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts
@@ -127,6 +127,9 @@ export async function POST(
actorEmail: session!.user.email ?? undefined,
resourceName: workflowRecord?.name ?? undefined,
description: `Reverted workflow to deployment version ${version}`,
+ metadata: {
+ targetVersion: version,
+ },
request,
})
diff --git a/apps/sim/app/api/workflows/[id]/duplicate/route.ts b/apps/sim/app/api/workflows/[id]/duplicate/route.ts
index 63c230f686b..0af8a82bae0 100644
--- a/apps/sim/app/api/workflows/[id]/duplicate/route.ts
+++ b/apps/sim/app/api/workflows/[id]/duplicate/route.ts
@@ -87,7 +87,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
resourceId: result.id,
resourceName: result.name,
description: `Duplicated workflow from ${sourceWorkflowId}`,
- metadata: { sourceWorkflowId },
+ metadata: {
+ sourceWorkflowId,
+ newWorkflowId: result.id,
+ folderId: folderId || undefined,
+ },
request: req,
})
diff --git a/apps/sim/app/api/workflows/[id]/restore/route.ts b/apps/sim/app/api/workflows/[id]/restore/route.ts
index a9d6b6ba1a5..c0b4d3d535f 100644
--- a/apps/sim/app/api/workflows/[id]/restore/route.ts
+++ b/apps/sim/app/api/workflows/[id]/restore/route.ts
@@ -56,6 +56,10 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
resourceId: workflowId,
resourceName: workflowData.name,
description: `Restored workflow "${workflowData.name}"`,
+ metadata: {
+ workflowName: workflowData.name,
+ workspaceId: workflowData.workspaceId || undefined,
+ },
request,
})
diff --git a/apps/sim/app/api/workflows/[id]/variables/route.ts b/apps/sim/app/api/workflows/[id]/variables/route.ts
index 1b4cd8ab3b9..064669c9b8f 100644
--- a/apps/sim/app/api/workflows/[id]/variables/route.ts
+++ b/apps/sim/app/api/workflows/[id]/variables/route.ts
@@ -90,7 +90,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
resourceId: workflowId,
resourceName: workflowData.name ?? undefined,
description: `Updated workflow variables`,
- metadata: { variableCount: Object.keys(variables).length },
+ metadata: {
+ variableCount: Object.keys(variables).length,
+ variableNames: Object.values(variables).map((v) => v.name),
+ workflowName: workflowData.name ?? undefined,
+ },
request: req,
})
diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts
index 3615afd0890..f96bd6d352f 100644
--- a/apps/sim/app/api/workflows/route.ts
+++ b/apps/sim/app/api/workflows/route.ts
@@ -296,7 +296,14 @@ export async function POST(req: NextRequest) {
resourceId: workflowId,
resourceName: name,
description: `Created workflow "${name}"`,
- metadata: { name },
+ metadata: {
+ name,
+ description: description || undefined,
+ color,
+ workspaceId,
+ folderId: folderId || undefined,
+ sortOrder,
+ },
request: req,
})
diff --git a/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts b/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts
index 42711f1fa8c..3345888a6f7 100644
--- a/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts
@@ -97,7 +97,12 @@ export async function PUT(
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
resourceName: name,
- description: `Updated workspace API key: ${name}`,
+ description: `Renamed workspace API key from "${existingKey[0].name}" to "${name}"`,
+ metadata: {
+ keyType: 'workspace',
+ previousName: existingKey[0].name,
+ newName: name,
+ },
request,
})
@@ -163,7 +168,11 @@ export async function DELETE(
actorEmail: session.user.email ?? undefined,
resourceName: deletedKey.name,
description: `Revoked workspace API key: ${deletedKey.name}`,
- metadata: { lastUsed: deletedKey.lastUsed?.toISOString() ?? null },
+ metadata: {
+ keyType: 'workspace',
+ keyName: deletedKey.name,
+ lastUsed: deletedKey.lastUsed?.toISOString() ?? null,
+ },
request,
})
diff --git a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts
index a6a15bb52f2..4c156d06f94 100644
--- a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts
@@ -182,7 +182,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
resourceId: newKey.id,
resourceName: name,
description: `Created API key "${name}"`,
- metadata: { keyName: name },
+ metadata: { keyName: name, keyType: 'workspace', source: source ?? 'settings' },
request,
})
@@ -257,8 +257,8 @@ export async function DELETE(
actorEmail: session?.user?.email,
action: AuditAction.API_KEY_REVOKED,
resourceType: AuditResourceType.API_KEY,
- description: `Revoked ${deletedCount} API key(s)`,
- metadata: { keyIds: keys, deletedCount },
+ description: `Revoked ${deletedCount} workspace API key(s)`,
+ metadata: { keyIds: keys, deletedCount, keyType: 'workspace' },
request,
})
diff --git a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts
index 65f177b1c55..5ccda1fae77 100644
--- a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts
@@ -172,6 +172,20 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
logger.info(`[${requestId}] Updated BYOK key for ${providerId} in workspace ${workspaceId}`)
+ recordAudit({
+ workspaceId,
+ actorId: userId,
+ actorName: session?.user?.name,
+ actorEmail: session?.user?.email,
+ action: AuditAction.BYOK_KEY_UPDATED,
+ resourceType: AuditResourceType.BYOK_KEY,
+ resourceId: existingKey[0].id,
+ resourceName: providerId,
+ description: `Updated BYOK key for ${providerId}`,
+ metadata: { providerId },
+ request,
+ })
+
return NextResponse.json({
success: true,
key: {
diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.ts b/apps/sim/app/api/workspaces/[id]/environment/route.ts
index 2e118b628d7..67b1eddeb7a 100644
--- a/apps/sim/app/api/workspaces/[id]/environment/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/environment/route.ts
@@ -140,8 +140,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
action: AuditAction.ENVIRONMENT_UPDATED,
resourceType: AuditResourceType.ENVIRONMENT,
resourceId: workspaceId,
- description: `Updated environment variables`,
- metadata: { variableCount: Object.keys(variables).length },
+ description: `Updated ${Object.keys(variables).length} workspace environment variable(s)`,
+ metadata: {
+ variableCount: Object.keys(variables).length,
+ updatedKeys: Object.keys(variables),
+ totalKeysAfterUpdate: Object.keys(merged).length,
+ },
request,
})
@@ -217,6 +221,22 @@ export async function DELETE(
actingUserId: userId,
})
+ recordAudit({
+ workspaceId,
+ actorId: userId,
+ actorName: session?.user?.name,
+ actorEmail: session?.user?.email,
+ action: AuditAction.ENVIRONMENT_DELETED,
+ resourceType: AuditResourceType.ENVIRONMENT,
+ resourceId: workspaceId,
+ description: `Removed ${keys.length} workspace environment variable(s)`,
+ metadata: {
+ removedKeys: keys,
+ remainingKeysCount: Object.keys(current).length,
+ },
+ request,
+ })
+
return NextResponse.json({ success: true })
} catch (error: any) {
logger.error(`[${requestId}] Workspace env DELETE error`, error)
diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts
index 24b5eb56cf0..179efc41d3f 100644
--- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts
@@ -69,7 +69,9 @@ export async function PUT(
action: AuditAction.FILE_UPDATED,
resourceType: AuditResourceType.FILE,
resourceId: fileId,
+ resourceName: updatedFile.name,
description: `Updated content of file "${updatedFile.name}"`,
+ metadata: { contentSize: buffer.length },
request,
})
diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts
index c440618863e..34cacc6808d 100644
--- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts
@@ -58,6 +58,7 @@ export async function PATCH(
action: AuditAction.FILE_UPDATED,
resourceType: AuditResourceType.FILE,
resourceId: fileId,
+ resourceName: updatedFile.name,
description: `Renamed file to "${updatedFile.name}"`,
request,
})
diff --git a/apps/sim/app/api/workspaces/[id]/files/route.ts b/apps/sim/app/api/workspaces/[id]/files/route.ts
index 5c887442796..41bdf82569f 100644
--- a/apps/sim/app/api/workspaces/[id]/files/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/files/route.ts
@@ -134,6 +134,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
resourceId: userFile.id,
resourceName: fileName,
description: `Uploaded file "${fileName}"`,
+ metadata: { fileSize: rawFile.size, fileType: rawFile.type || 'application/octet-stream' },
request,
})
diff --git a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts
index 08d3f5802d2..ae5ae96c3e6 100644
--- a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts
@@ -262,6 +262,14 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
description: `Updated ${subscription.notificationType} notification subscription`,
+ metadata: {
+ notificationType: subscription.notificationType,
+ updatedFields: Object.keys(data).filter(
+ (k) => (data as Record)[k] !== undefined
+ ),
+ ...(data.active !== undefined && { active: data.active }),
+ ...(data.alertConfig !== undefined && { alertRule: data.alertConfig?.rule ?? null }),
+ },
request,
})
@@ -340,6 +348,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
actorEmail: session.user.email ?? undefined,
resourceName: deletedSubscription.notificationType,
description: `Deleted ${deletedSubscription.notificationType} notification subscription`,
+ metadata: {
+ notificationType: deletedSubscription.notificationType,
+ },
request,
})
diff --git a/apps/sim/app/api/workspaces/[id]/notifications/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/route.ts
index 1a18f8d2386..3ad7532f8e8 100644
--- a/apps/sim/app/api/workspaces/[id]/notifications/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/notifications/route.ts
@@ -278,6 +278,17 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
description: `Created ${data.notificationType} notification subscription`,
+ metadata: {
+ notificationType: data.notificationType,
+ allWorkflows: data.allWorkflows,
+ workflowCount: data.workflowIds.length,
+ levelFilter: data.levelFilter,
+ alertRule: data.alertConfig?.rule ?? null,
+ ...(data.notificationType === 'email' && {
+ recipientCount: data.emailRecipients?.length ?? 0,
+ }),
+ ...(data.notificationType === 'slack' && { channelName: data.slackConfig?.channelName }),
+ },
request,
})
diff --git a/apps/sim/app/api/workspaces/[id]/permissions/route.ts b/apps/sim/app/api/workspaces/[id]/permissions/route.ts
index f31bce34ba6..e7ee5385597 100644
--- a/apps/sim/app/api/workspaces/[id]/permissions/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/permissions/route.ts
@@ -202,19 +202,15 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
action: AuditAction.MEMBER_ROLE_CHANGED,
resourceType: AuditResourceType.WORKSPACE,
resourceId: workspaceId,
+ resourceName: permLookup.get(update.userId)?.email ?? update.userId,
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
- description: `Changed permissions for user ${update.userId} to ${update.permissions}`,
+ description: `Changed permissions for ${permLookup.get(update.userId)?.email ?? update.userId} from ${permLookup.get(update.userId)?.permission ?? 'none'} to ${update.permissions}`,
metadata: {
targetUserId: update.userId,
targetEmail: permLookup.get(update.userId)?.email ?? undefined,
- changes: [
- {
- field: 'permissions',
- from: permLookup.get(update.userId)?.permission ?? null,
- to: update.permissions,
- },
- ],
+ previousRole: permLookup.get(update.userId)?.permission ?? null,
+ newRole: update.permissions,
},
request,
})
diff --git a/apps/sim/app/api/workspaces/[id]/route.ts b/apps/sim/app/api/workspaces/[id]/route.ts
index 375e0879b8b..ca4e9408fcf 100644
--- a/apps/sim/app/api/workspaces/[id]/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/route.ts
@@ -202,6 +202,37 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
.where(eq(workspace.id, workspaceId))
.then((rows) => rows[0])
+ recordAudit({
+ workspaceId,
+ actorId: session.user.id,
+ actorName: session.user.name,
+ actorEmail: session.user.email,
+ action: AuditAction.WORKSPACE_UPDATED,
+ resourceType: AuditResourceType.WORKSPACE,
+ resourceId: workspaceId,
+ resourceName: updatedWorkspace?.name ?? existingWorkspace.name,
+ description: `Updated workspace "${updatedWorkspace?.name ?? existingWorkspace.name}"`,
+ metadata: {
+ changes: {
+ ...(name !== undefined && { name: { from: existingWorkspace.name, to: name } }),
+ ...(color !== undefined && { color: { from: existingWorkspace.color, to: color } }),
+ ...(allowPersonalApiKeys !== undefined && {
+ allowPersonalApiKeys: {
+ from: existingWorkspace.allowPersonalApiKeys,
+ to: allowPersonalApiKeys,
+ },
+ }),
+ ...(billedAccountUserId !== undefined && {
+ billedAccountUserId: {
+ from: existingWorkspace.billedAccountUserId,
+ to: billedAccountUserId,
+ },
+ }),
+ },
+ },
+ request,
+ })
+
return NextResponse.json({
workspace: {
...updatedWorkspace,
diff --git a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts
index 602b60e88cd..d76322d4e5f 100644
--- a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts
+++ b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts
@@ -189,7 +189,13 @@ export async function GET(
actorEmail: session.user.email ?? undefined,
resourceName: workspaceDetails.name,
description: `Accepted workspace invitation to "${workspaceDetails.name}"`,
- metadata: { targetEmail: invitation.email },
+ metadata: {
+ targetEmail: invitation.email,
+ workspaceName: workspaceDetails.name,
+ assignedPermission: invitation.permissions || 'read',
+ invitationId: invitation.id,
+ inviterId: invitation.inviterId,
+ },
request: req,
})
@@ -272,7 +278,11 @@ export async function DELETE(
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
description: `Revoked workspace invitation for ${invitation.email}`,
- metadata: { invitationId, targetEmail: invitation.email },
+ metadata: {
+ invitationId,
+ targetEmail: invitation.email,
+ invitationStatus: invitation.status,
+ },
request: _request,
})
@@ -360,6 +370,24 @@ export async function POST(
)
}
+ recordAudit({
+ workspaceId: invitation.workspaceId,
+ actorId: session.user.id,
+ action: AuditAction.INVITATION_RESENT,
+ resourceType: AuditResourceType.WORKSPACE,
+ resourceId: invitation.workspaceId,
+ actorName: session.user.name ?? undefined,
+ actorEmail: session.user.email ?? undefined,
+ resourceName: ws.name,
+ description: `Resent workspace invitation to ${invitation.email}`,
+ metadata: {
+ invitationId,
+ targetEmail: invitation.email,
+ workspaceName: ws.name,
+ },
+ request: _request,
+ })
+
return NextResponse.json({ success: true })
} catch (error) {
logger.error('Error resending workspace invitation:', error)
diff --git a/apps/sim/app/api/workspaces/invitations/route.ts b/apps/sim/app/api/workspaces/invitations/route.ts
index 30c91acd22f..020e350dbb2 100644
--- a/apps/sim/app/api/workspaces/invitations/route.ts
+++ b/apps/sim/app/api/workspaces/invitations/route.ts
@@ -243,7 +243,12 @@ export async function POST(req: NextRequest) {
resourceId: workspaceId,
resourceName: email,
description: `Invited ${email} as ${permission}`,
- metadata: { targetEmail: email, targetRole: permission },
+ metadata: {
+ targetEmail: email,
+ targetRole: permission,
+ workspaceName: workspaceDetails.name,
+ invitationId: invitationData.id,
+ },
request: req,
})
diff --git a/apps/sim/app/api/workspaces/members/[id]/route.ts b/apps/sim/app/api/workspaces/members/[id]/route.ts
index ca918712946..e4a507c5a78 100644
--- a/apps/sim/app/api/workspaces/members/[id]/route.ts
+++ b/apps/sim/app/api/workspaces/members/[id]/route.ts
@@ -121,8 +121,12 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
action: AuditAction.MEMBER_REMOVED,
resourceType: AuditResourceType.WORKSPACE,
resourceId: workspaceId,
- description: isSelf ? 'Left the workspace' : 'Removed a member from the workspace',
- metadata: { removedUserId: userId, selfRemoval: isSelf },
+ description: isSelf ? 'Left the workspace' : `Removed member ${userId} from the workspace`,
+ metadata: {
+ removedUserId: userId,
+ removedUserRole: userPermission.permissionType,
+ selfRemoval: isSelf,
+ },
request: req,
})
diff --git a/apps/sim/app/api/workspaces/route.ts b/apps/sim/app/api/workspaces/route.ts
index d64fd05f758..1fdc15f94df 100644
--- a/apps/sim/app/api/workspaces/route.ts
+++ b/apps/sim/app/api/workspaces/route.ts
@@ -118,7 +118,7 @@ export async function POST(req: Request) {
resourceId: newWorkspace.id,
resourceName: newWorkspace.name,
description: `Created workspace "${newWorkspace.name}"`,
- metadata: { name: newWorkspace.name },
+ metadata: { name: newWorkspace.name, color: newWorkspace.color },
request: req,
})
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx
index 4642fc9e843..b6f439635b9 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx
@@ -27,6 +27,7 @@ import {
isBillingEnabled,
isCredentialSetsEnabled,
} from '@/app/workspace/[workspaceId]/settings/navigation'
+import { AuditLogsSkeleton } from '@/ee/audit-logs/components/audit-logs-skeleton'
/**
* Generic skeleton fallback for sections without a dedicated skeleton.
@@ -153,6 +154,10 @@ const AccessControl = dynamic(
() => import('@/ee/access-control/components/access-control').then((m) => m.AccessControl),
{ loading: () => }
)
+const AuditLogs = dynamic(
+ () => import('@/ee/audit-logs/components/audit-logs').then((m) => m.AuditLogs),
+ { loading: () => }
+)
const SSO = dynamic(() => import('@/ee/sso/components/sso-settings').then((m) => m.SSO), {
loading: () => ,
})
@@ -201,6 +206,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
{/* {effectiveSection === 'template-profile' && } */}
{effectiveSection === 'credential-sets' && }
{effectiveSection === 'access-control' && }
+ {effectiveSection === 'audit-logs' && }
{effectiveSection === 'apikeys' && }
{isBillingEnabled && effectiveSection === 'subscription' && }
{isBillingEnabled && effectiveSection === 'team' && }
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts
index eb6941cb10b..ff25389fc0c 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts
+++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts
@@ -1,5 +1,6 @@
import {
Card,
+ ClipboardList,
Connections,
HexSimple,
Key,
@@ -27,6 +28,7 @@ export type SettingsSection =
| 'template-profile'
| 'credential-sets'
| 'access-control'
+ | 'audit-logs'
| 'apikeys'
| 'byok'
| 'subscription'
@@ -97,6 +99,14 @@ export const allNavigationItems: NavigationItem[] = [
requiresEnterprise: true,
selfHostedOverride: isAccessControlEnabled,
},
+ {
+ id: 'audit-logs',
+ label: 'Audit Logs',
+ icon: ClipboardList,
+ section: 'enterprise',
+ requiresHosted: true,
+ requiresEnterprise: true,
+ },
{
id: 'subscription',
label: 'Subscription',
diff --git a/apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx b/apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx
new file mode 100644
index 00000000000..ae5504c7ffc
--- /dev/null
+++ b/apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx
@@ -0,0 +1,27 @@
+import { Skeleton } from '@/components/emcn'
+
+export function AuditLogsSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+
+
+
+
+
+ ))}
+
+ )
+}
diff --git a/apps/sim/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx
new file mode 100644
index 00000000000..7aef8bd7946
--- /dev/null
+++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx
@@ -0,0 +1,267 @@
+'use client'
+
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { createLogger } from '@sim/logger'
+import { RefreshCw, Search } from 'lucide-react'
+import { Badge, Button, Combobox, type ComboboxOption, Skeleton } from '@/components/emcn'
+import { Input } from '@/components/ui'
+import { cn } from '@/lib/core/utils/cn'
+import { formatDateTime } from '@/lib/core/utils/formatting'
+import type { EnterpriseAuditLogEntry } from '@/app/api/v1/audit-logs/format'
+import { RESOURCE_TYPE_OPTIONS } from '@/ee/audit-logs/constants'
+import { type AuditLogFilters, useAuditLogs } from '@/ee/audit-logs/hooks/audit-logs'
+
+const logger = createLogger('AuditLogs')
+
+const DATE_RANGE_OPTIONS: ComboboxOption[] = [
+ { label: 'Last 7 days', value: '7' },
+ { label: 'Last 30 days', value: '30' },
+ { label: 'Last 90 days', value: '90' },
+ { label: 'All time', value: '' },
+]
+
+function formatResourceType(type: string): string {
+ return type
+ .split('_')
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
+ .join(' ')
+}
+
+function getStartOfDay(daysAgo: number): string {
+ const start = new Date()
+ start.setDate(start.getDate() - daysAgo)
+ start.setHours(0, 0, 0, 0)
+ return start.toISOString()
+}
+
+function formatAction(action: string): string {
+ return action.replace(/[._]/g, ' ')
+}
+
+interface ActionBadgeProps {
+ action: string
+}
+
+function ActionBadge({ action }: ActionBadgeProps) {
+ const [, verb] = action.split('.')
+ const variant = verb === 'deleted' || verb === 'removed' || verb === 'revoked' ? 'red' : 'default'
+ return (
+
+ {formatAction(action)}
+
+ )
+}
+
+interface AuditLogRowProps {
+ entry: EnterpriseAuditLogEntry
+}
+
+function AuditLogRow({ entry }: AuditLogRowProps) {
+ const [expanded, setExpanded] = useState(false)
+ const timestamp = formatDateTime(new Date(entry.createdAt))
+
+ return (
+
+
setExpanded(!expanded)}
+ >
+
+ {timestamp}
+
+
+
+
+
+ {entry.description || entry.resourceName || entry.resourceId || '-'}
+
+
+ {entry.actorEmail || entry.actorName || 'System'}
+
+
+ {expanded && (
+
+
+ Resource
+
+ {formatResourceType(entry.resourceType)}
+ {entry.resourceId && (
+ ({entry.resourceId})
+ )}
+
+
+ {entry.resourceName && (
+
+ Name
+ {entry.resourceName}
+
+ )}
+
+ Actor
+
+ {entry.actorName || 'Unknown'}
+ {entry.actorEmail && (
+ ({entry.actorEmail})
+ )}
+
+
+ {entry.description && (
+
+ Description
+ {entry.description}
+
+ )}
+ {entry.metadata != null &&
+ Object.keys(entry.metadata as Record
).length > 0 ? (
+
+
Details
+
+ {JSON.stringify(entry.metadata, null, 2)}
+
+
+ ) : null}
+
+ )}
+
+ )
+}
+
+export function AuditLogs() {
+ const [resourceType, setResourceType] = useState('')
+ const [dateRange, setDateRange] = useState('30')
+ const [searchTerm, setSearchTerm] = useState('')
+ const [debouncedSearch, setDebouncedSearch] = useState('')
+ const debounceRef = useRef | null>(null)
+
+ useEffect(() => {
+ const trimmed = searchTerm.trim()
+ if (trimmed === debouncedSearch) return
+ debounceRef.current = setTimeout(() => {
+ setDebouncedSearch(trimmed)
+ }, 300)
+ return () => {
+ if (debounceRef.current) clearTimeout(debounceRef.current)
+ }
+ }, [searchTerm, debouncedSearch])
+
+ const filters = useMemo(() => {
+ return {
+ search: debouncedSearch || undefined,
+ resourceType: resourceType || undefined,
+ startDate: dateRange ? getStartOfDay(Number(dateRange)) : undefined,
+ }
+ }, [debouncedSearch, resourceType, dateRange])
+
+ const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage, refetch, isRefetching } =
+ useAuditLogs(filters)
+
+ const allEntries = useMemo(() => {
+ if (!data?.pages) return []
+ return data.pages.flatMap((page) => page.data)
+ }, [data])
+
+ const handleRefresh = useCallback(() => {
+ refetch().catch((error: unknown) => {
+ logger.error('Failed to refresh audit logs', { error })
+ })
+ }, [refetch])
+
+ const handleLoadMore = useCallback(() => {
+ if (hasNextPage && !isFetchingNextPage) {
+ fetchNextPage().catch((error: unknown) => {
+ logger.error('Failed to load more audit logs', { error })
+ })
+ }
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage])
+
+ return (
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Timestamp
+
+
+ Event
+
+
+ Description
+
+
+ Actor
+
+
+
+
+ {isLoading ? (
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+
+
+
+
+
+ ))}
+
+ ) : allEntries.length === 0 ? (
+
+ {debouncedSearch ? `No results for "${debouncedSearch}"` : 'No audit logs found'}
+
+ ) : (
+
+ {allEntries.map((entry) => (
+
+ ))}
+ {hasNextPage && (
+
+
+ {isFetchingNextPage ? 'Loading...' : 'Load more'}
+
+
+ )}
+
+ )}
+
+
+ )
+}
diff --git a/apps/sim/ee/audit-logs/constants.ts b/apps/sim/ee/audit-logs/constants.ts
new file mode 100644
index 00000000000..445265f4f81
--- /dev/null
+++ b/apps/sim/ee/audit-logs/constants.ts
@@ -0,0 +1,24 @@
+import type { ComboboxOption } from '@/components/emcn'
+import { AuditResourceType } from '@/lib/audit/types'
+
+const ACRONYMS = new Set(['API', 'BYOK', 'MCP', 'OAUTH'])
+
+const DISPLAY_OVERRIDES: Record = { OAUTH: 'OAuth' }
+
+function formatResourceLabel(key: string): string {
+ return key
+ .split('_')
+ .map((w) => {
+ const upper = w.toUpperCase()
+ if (ACRONYMS.has(upper)) return DISPLAY_OVERRIDES[upper] ?? upper
+ return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
+ })
+ .join(' ')
+}
+
+export const RESOURCE_TYPE_OPTIONS: ComboboxOption[] = [
+ { label: 'All Types', value: '' },
+ ...(Object.entries(AuditResourceType) as [string, string][])
+ .map(([key, value]) => ({ label: formatResourceLabel(key), value }))
+ .sort((a, b) => a.label.localeCompare(b.label)),
+]
diff --git a/apps/sim/ee/audit-logs/hooks/audit-logs.ts b/apps/sim/ee/audit-logs/hooks/audit-logs.ts
new file mode 100644
index 00000000000..259d6094c0d
--- /dev/null
+++ b/apps/sim/ee/audit-logs/hooks/audit-logs.ts
@@ -0,0 +1,58 @@
+import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query'
+import type { EnterpriseAuditLogEntry } from '@/app/api/v1/audit-logs/format'
+
+export const auditLogKeys = {
+ all: ['audit-logs'] as const,
+ lists: () => [...auditLogKeys.all, 'list'] as const,
+ list: (filters: AuditLogFilters) => [...auditLogKeys.lists(), filters] as const,
+}
+
+export interface AuditLogFilters {
+ search?: string
+ action?: string
+ resourceType?: string
+ actorId?: string
+ startDate?: string
+ endDate?: string
+}
+
+interface AuditLogPage {
+ success: boolean
+ data: EnterpriseAuditLogEntry[]
+ nextCursor?: string
+}
+
+async function fetchAuditLogs(
+ filters: AuditLogFilters,
+ cursor?: string,
+ signal?: AbortSignal
+): Promise {
+ const params = new URLSearchParams()
+ params.set('limit', '50')
+ if (filters.search) params.set('search', filters.search)
+ if (filters.action) params.set('action', filters.action)
+ if (filters.resourceType) params.set('resourceType', filters.resourceType)
+ if (filters.actorId) params.set('actorId', filters.actorId)
+ if (filters.startDate) params.set('startDate', filters.startDate)
+ if (filters.endDate) params.set('endDate', filters.endDate)
+ if (cursor) params.set('cursor', cursor)
+
+ const response = await fetch(`/api/audit-logs?${params.toString()}`, { signal })
+ if (!response.ok) {
+ const body = await response.json().catch(() => ({}))
+ throw new Error(body.error || `Failed to fetch audit logs: ${response.status}`)
+ }
+ return response.json()
+}
+
+export function useAuditLogs(filters: AuditLogFilters, enabled = true) {
+ return useInfiniteQuery({
+ queryKey: auditLogKeys.list(filters),
+ queryFn: ({ pageParam, signal }) => fetchAuditLogs(filters, pageParam, signal),
+ initialPageParam: undefined as string | undefined,
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
+ enabled,
+ staleTime: 30 * 1000,
+ placeholderData: keepPreviousData,
+ })
+}
diff --git a/apps/sim/lib/audit/log.ts b/apps/sim/lib/audit/log.ts
index fc0e8ba3fc6..ea7783aba78 100644
--- a/apps/sim/lib/audit/log.ts
+++ b/apps/sim/lib/audit/log.ts
@@ -2,214 +2,14 @@ import { auditLog, db } from '@sim/db'
import { user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
+import type { AuditActionType, AuditResourceTypeValue } from '@/lib/audit/types'
import { getClientIp } from '@/lib/core/utils/request'
import { generateShortId } from '@/lib/core/utils/uuid'
-const logger = createLogger('AuditLog')
-
-/**
- * All auditable actions in the platform, grouped by resource type.
- */
-export const AuditAction = {
- // API Keys
- API_KEY_CREATED: 'api_key.created',
- API_KEY_UPDATED: 'api_key.updated',
- API_KEY_REVOKED: 'api_key.revoked',
- PERSONAL_API_KEY_CREATED: 'personal_api_key.created',
- PERSONAL_API_KEY_REVOKED: 'personal_api_key.revoked',
-
- // BYOK Keys
- BYOK_KEY_CREATED: 'byok_key.created',
- BYOK_KEY_DELETED: 'byok_key.deleted',
-
- // Chat
- CHAT_DEPLOYED: 'chat.deployed',
- CHAT_UPDATED: 'chat.updated',
- CHAT_DELETED: 'chat.deleted',
-
- // Custom Tools
- CUSTOM_TOOL_CREATED: 'custom_tool.created',
- CUSTOM_TOOL_UPDATED: 'custom_tool.updated',
- CUSTOM_TOOL_DELETED: 'custom_tool.deleted',
-
- // Billing
- CREDIT_PURCHASED: 'credit.purchased',
-
- // Credential Sets
- CREDENTIAL_SET_CREATED: 'credential_set.created',
- CREDENTIAL_SET_UPDATED: 'credential_set.updated',
- CREDENTIAL_SET_DELETED: 'credential_set.deleted',
- CREDENTIAL_SET_MEMBER_REMOVED: 'credential_set_member.removed',
- CREDENTIAL_SET_MEMBER_LEFT: 'credential_set_member.left',
- CREDENTIAL_SET_INVITATION_CREATED: 'credential_set_invitation.created',
- CREDENTIAL_SET_INVITATION_ACCEPTED: 'credential_set_invitation.accepted',
- CREDENTIAL_SET_INVITATION_RESENT: 'credential_set_invitation.resent',
- CREDENTIAL_SET_INVITATION_REVOKED: 'credential_set_invitation.revoked',
-
- // Connector Documents
- CONNECTOR_DOCUMENT_RESTORED: 'connector_document.restored',
- CONNECTOR_DOCUMENT_EXCLUDED: 'connector_document.excluded',
-
- // Documents
- DOCUMENT_UPLOADED: 'document.uploaded',
- DOCUMENT_UPDATED: 'document.updated',
- DOCUMENT_DELETED: 'document.deleted',
-
- // Environment
- ENVIRONMENT_UPDATED: 'environment.updated',
-
- // Files
- FILE_UPLOADED: 'file.uploaded',
- FILE_UPDATED: 'file.updated',
- FILE_DELETED: 'file.deleted',
- FILE_RESTORED: 'file.restored',
-
- // Folders
- FOLDER_CREATED: 'folder.created',
- FOLDER_DELETED: 'folder.deleted',
- FOLDER_DUPLICATED: 'folder.duplicated',
- FOLDER_RESTORED: 'folder.restored',
-
- // Forms
- FORM_CREATED: 'form.created',
- FORM_UPDATED: 'form.updated',
- FORM_DELETED: 'form.deleted',
-
- // Invitations
- INVITATION_ACCEPTED: 'invitation.accepted',
- INVITATION_REVOKED: 'invitation.revoked',
-
- // Knowledge Base Connectors
- CONNECTOR_CREATED: 'connector.created',
- CONNECTOR_UPDATED: 'connector.updated',
- CONNECTOR_DELETED: 'connector.deleted',
- CONNECTOR_SYNCED: 'connector.synced',
-
- // Knowledge Bases
- KNOWLEDGE_BASE_CREATED: 'knowledge_base.created',
- KNOWLEDGE_BASE_UPDATED: 'knowledge_base.updated',
- KNOWLEDGE_BASE_DELETED: 'knowledge_base.deleted',
- KNOWLEDGE_BASE_RESTORED: 'knowledge_base.restored',
-
- // MCP Servers
- MCP_SERVER_ADDED: 'mcp_server.added',
- MCP_SERVER_UPDATED: 'mcp_server.updated',
- MCP_SERVER_REMOVED: 'mcp_server.removed',
+export type { AuditActionType, AuditResourceTypeValue } from '@/lib/audit/types'
+export { AuditAction, AuditResourceType } from '@/lib/audit/types'
- // Members
- MEMBER_INVITED: 'member.invited',
- MEMBER_REMOVED: 'member.removed',
- MEMBER_ROLE_CHANGED: 'member.role_changed',
-
- // Notifications
- NOTIFICATION_CREATED: 'notification.created',
- NOTIFICATION_UPDATED: 'notification.updated',
- NOTIFICATION_DELETED: 'notification.deleted',
-
- // OAuth / Credentials
- OAUTH_DISCONNECTED: 'oauth.disconnected',
- CREDENTIAL_RENAMED: 'credential.renamed',
- CREDENTIAL_DELETED: 'credential.deleted',
-
- // Password
- PASSWORD_RESET: 'password.reset',
-
- // Organizations
- ORGANIZATION_CREATED: 'organization.created',
- ORGANIZATION_UPDATED: 'organization.updated',
- ORG_MEMBER_ADDED: 'org_member.added',
- ORG_MEMBER_REMOVED: 'org_member.removed',
- ORG_MEMBER_ROLE_CHANGED: 'org_member.role_changed',
- ORG_INVITATION_CREATED: 'org_invitation.created',
- ORG_INVITATION_ACCEPTED: 'org_invitation.accepted',
- ORG_INVITATION_REJECTED: 'org_invitation.rejected',
- ORG_INVITATION_CANCELLED: 'org_invitation.cancelled',
- ORG_INVITATION_REVOKED: 'org_invitation.revoked',
-
- // Permission Groups
- PERMISSION_GROUP_CREATED: 'permission_group.created',
- PERMISSION_GROUP_UPDATED: 'permission_group.updated',
- PERMISSION_GROUP_DELETED: 'permission_group.deleted',
- PERMISSION_GROUP_MEMBER_ADDED: 'permission_group_member.added',
- PERMISSION_GROUP_MEMBER_REMOVED: 'permission_group_member.removed',
-
- // Skills
- SKILL_CREATED: 'skill.created',
- SKILL_UPDATED: 'skill.updated',
- SKILL_DELETED: 'skill.deleted',
-
- // Schedules
- SCHEDULE_UPDATED: 'schedule.updated',
-
- // Tables
- TABLE_CREATED: 'table.created',
- TABLE_UPDATED: 'table.updated',
- TABLE_DELETED: 'table.deleted',
- TABLE_RESTORED: 'table.restored',
-
- // Templates
- TEMPLATE_CREATED: 'template.created',
- TEMPLATE_UPDATED: 'template.updated',
- TEMPLATE_DELETED: 'template.deleted',
-
- // Webhooks
- WEBHOOK_CREATED: 'webhook.created',
- WEBHOOK_DELETED: 'webhook.deleted',
-
- // Workflows
- WORKFLOW_CREATED: 'workflow.created',
- WORKFLOW_DELETED: 'workflow.deleted',
- WORKFLOW_RESTORED: 'workflow.restored',
- WORKFLOW_DEPLOYED: 'workflow.deployed',
- WORKFLOW_UNDEPLOYED: 'workflow.undeployed',
- WORKFLOW_DUPLICATED: 'workflow.duplicated',
- WORKFLOW_DEPLOYMENT_ACTIVATED: 'workflow.deployment_activated',
- WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted',
- WORKFLOW_LOCKED: 'workflow.locked',
- WORKFLOW_UNLOCKED: 'workflow.unlocked',
- WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated',
-
- // Workspaces
- WORKSPACE_CREATED: 'workspace.created',
- WORKSPACE_DELETED: 'workspace.deleted',
- WORKSPACE_DUPLICATED: 'workspace.duplicated',
-} as const
-
-export type AuditActionType = (typeof AuditAction)[keyof typeof AuditAction]
-
-/**
- * All resource types that can appear in audit log entries.
- */
-export const AuditResourceType = {
- API_KEY: 'api_key',
- BILLING: 'billing',
- BYOK_KEY: 'byok_key',
- CHAT: 'chat',
- CONNECTOR: 'connector',
- CREDENTIAL_SET: 'credential_set',
- CUSTOM_TOOL: 'custom_tool',
- DOCUMENT: 'document',
- ENVIRONMENT: 'environment',
- FILE: 'file',
- FOLDER: 'folder',
- FORM: 'form',
- KNOWLEDGE_BASE: 'knowledge_base',
- MCP_SERVER: 'mcp_server',
- NOTIFICATION: 'notification',
- OAUTH: 'oauth',
- ORGANIZATION: 'organization',
- PASSWORD: 'password',
- PERMISSION_GROUP: 'permission_group',
- SCHEDULE: 'schedule',
- SKILL: 'skill',
- TABLE: 'table',
- TEMPLATE: 'template',
- WEBHOOK: 'webhook',
- WORKFLOW: 'workflow',
- WORKSPACE: 'workspace',
-} as const
-
-export type AuditResourceTypeValue = (typeof AuditResourceType)[keyof typeof AuditResourceType]
+const logger = createLogger('AuditLog')
interface AuditLogParams {
workspaceId?: string | null
diff --git a/apps/sim/lib/audit/types.ts b/apps/sim/lib/audit/types.ts
new file mode 100644
index 00000000000..bc1f857f469
--- /dev/null
+++ b/apps/sim/lib/audit/types.ts
@@ -0,0 +1,214 @@
+/**
+ * All auditable actions in the platform, grouped by resource type.
+ */
+export const AuditAction = {
+ // API Keys
+ API_KEY_CREATED: 'api_key.created',
+ API_KEY_UPDATED: 'api_key.updated',
+ API_KEY_REVOKED: 'api_key.revoked',
+ PERSONAL_API_KEY_CREATED: 'personal_api_key.created',
+ PERSONAL_API_KEY_REVOKED: 'personal_api_key.revoked',
+
+ // BYOK Keys
+ BYOK_KEY_CREATED: 'byok_key.created',
+ BYOK_KEY_UPDATED: 'byok_key.updated',
+ BYOK_KEY_DELETED: 'byok_key.deleted',
+
+ // Chat
+ CHAT_DEPLOYED: 'chat.deployed',
+ CHAT_UPDATED: 'chat.updated',
+ CHAT_DELETED: 'chat.deleted',
+
+ // Custom Tools
+ CUSTOM_TOOL_CREATED: 'custom_tool.created',
+ CUSTOM_TOOL_UPDATED: 'custom_tool.updated',
+ CUSTOM_TOOL_DELETED: 'custom_tool.deleted',
+
+ // Billing
+ CREDIT_PURCHASED: 'credit.purchased',
+
+ // Credential Sets
+ CREDENTIAL_SET_CREATED: 'credential_set.created',
+ CREDENTIAL_SET_UPDATED: 'credential_set.updated',
+ CREDENTIAL_SET_DELETED: 'credential_set.deleted',
+ CREDENTIAL_SET_MEMBER_REMOVED: 'credential_set_member.removed',
+ CREDENTIAL_SET_MEMBER_LEFT: 'credential_set_member.left',
+ CREDENTIAL_SET_INVITATION_CREATED: 'credential_set_invitation.created',
+ CREDENTIAL_SET_INVITATION_ACCEPTED: 'credential_set_invitation.accepted',
+ CREDENTIAL_SET_INVITATION_RESENT: 'credential_set_invitation.resent',
+ CREDENTIAL_SET_INVITATION_REVOKED: 'credential_set_invitation.revoked',
+
+ // Connector Documents
+ CONNECTOR_DOCUMENT_RESTORED: 'connector_document.restored',
+ CONNECTOR_DOCUMENT_EXCLUDED: 'connector_document.excluded',
+
+ // Documents
+ DOCUMENT_UPLOADED: 'document.uploaded',
+ DOCUMENT_UPDATED: 'document.updated',
+ DOCUMENT_DELETED: 'document.deleted',
+
+ // Environment
+ ENVIRONMENT_UPDATED: 'environment.updated',
+ ENVIRONMENT_DELETED: 'environment.deleted',
+
+ // Files
+ FILE_UPLOADED: 'file.uploaded',
+ FILE_UPDATED: 'file.updated',
+ FILE_DELETED: 'file.deleted',
+ FILE_RESTORED: 'file.restored',
+
+ // Folders
+ FOLDER_CREATED: 'folder.created',
+ FOLDER_DELETED: 'folder.deleted',
+ FOLDER_DUPLICATED: 'folder.duplicated',
+ FOLDER_RESTORED: 'folder.restored',
+
+ // Forms
+ FORM_CREATED: 'form.created',
+ FORM_UPDATED: 'form.updated',
+ FORM_DELETED: 'form.deleted',
+
+ // Invitations
+ INVITATION_ACCEPTED: 'invitation.accepted',
+ INVITATION_RESENT: 'invitation.resent',
+ INVITATION_REVOKED: 'invitation.revoked',
+
+ // Knowledge Base Connectors
+ CONNECTOR_CREATED: 'connector.created',
+ CONNECTOR_UPDATED: 'connector.updated',
+ CONNECTOR_DELETED: 'connector.deleted',
+ CONNECTOR_SYNCED: 'connector.synced',
+
+ // Knowledge Bases
+ KNOWLEDGE_BASE_CREATED: 'knowledge_base.created',
+ KNOWLEDGE_BASE_UPDATED: 'knowledge_base.updated',
+ KNOWLEDGE_BASE_DELETED: 'knowledge_base.deleted',
+ KNOWLEDGE_BASE_RESTORED: 'knowledge_base.restored',
+
+ // MCP Servers
+ MCP_SERVER_ADDED: 'mcp_server.added',
+ MCP_SERVER_UPDATED: 'mcp_server.updated',
+ MCP_SERVER_REMOVED: 'mcp_server.removed',
+
+ // Members
+ MEMBER_INVITED: 'member.invited',
+ MEMBER_REMOVED: 'member.removed',
+ MEMBER_ROLE_CHANGED: 'member.role_changed',
+
+ // Notifications
+ NOTIFICATION_CREATED: 'notification.created',
+ NOTIFICATION_UPDATED: 'notification.updated',
+ NOTIFICATION_DELETED: 'notification.deleted',
+
+ // OAuth / Credentials
+ OAUTH_DISCONNECTED: 'oauth.disconnected',
+ CREDENTIAL_CREATED: 'credential.created',
+ CREDENTIAL_UPDATED: 'credential.updated',
+ CREDENTIAL_RENAMED: 'credential.renamed',
+ CREDENTIAL_DELETED: 'credential.deleted',
+
+ // Password
+ PASSWORD_RESET_REQUESTED: 'password.reset_requested',
+ PASSWORD_RESET: 'password.reset',
+
+ // Organizations
+ ORGANIZATION_CREATED: 'organization.created',
+ ORGANIZATION_UPDATED: 'organization.updated',
+ ORG_MEMBER_ADDED: 'org_member.added',
+ ORG_MEMBER_REMOVED: 'org_member.removed',
+ ORG_MEMBER_ROLE_CHANGED: 'org_member.role_changed',
+ ORG_INVITATION_CREATED: 'org_invitation.created',
+ ORG_INVITATION_ACCEPTED: 'org_invitation.accepted',
+ ORG_INVITATION_REJECTED: 'org_invitation.rejected',
+ ORG_INVITATION_CANCELLED: 'org_invitation.cancelled',
+ ORG_INVITATION_REVOKED: 'org_invitation.revoked',
+ ORG_INVITATION_RESENT: 'org_invitation.resent',
+
+ // Permission Groups
+ PERMISSION_GROUP_CREATED: 'permission_group.created',
+ PERMISSION_GROUP_UPDATED: 'permission_group.updated',
+ PERMISSION_GROUP_DELETED: 'permission_group.deleted',
+ PERMISSION_GROUP_MEMBER_ADDED: 'permission_group_member.added',
+ PERMISSION_GROUP_MEMBER_REMOVED: 'permission_group_member.removed',
+
+ // Skills
+ SKILL_CREATED: 'skill.created',
+ SKILL_UPDATED: 'skill.updated',
+ SKILL_DELETED: 'skill.deleted',
+
+ // Schedules
+ SCHEDULE_CREATED: 'schedule.created',
+ SCHEDULE_UPDATED: 'schedule.updated',
+ SCHEDULE_DELETED: 'schedule.deleted',
+
+ // Tables
+ TABLE_CREATED: 'table.created',
+ TABLE_UPDATED: 'table.updated',
+ TABLE_DELETED: 'table.deleted',
+ TABLE_RESTORED: 'table.restored',
+
+ // Templates
+ TEMPLATE_CREATED: 'template.created',
+ TEMPLATE_UPDATED: 'template.updated',
+ TEMPLATE_DELETED: 'template.deleted',
+
+ // Webhooks
+ WEBHOOK_CREATED: 'webhook.created',
+ WEBHOOK_DELETED: 'webhook.deleted',
+
+ // Workflows
+ WORKFLOW_CREATED: 'workflow.created',
+ WORKFLOW_DELETED: 'workflow.deleted',
+ WORKFLOW_RESTORED: 'workflow.restored',
+ WORKFLOW_DEPLOYED: 'workflow.deployed',
+ WORKFLOW_UNDEPLOYED: 'workflow.undeployed',
+ WORKFLOW_DUPLICATED: 'workflow.duplicated',
+ WORKFLOW_DEPLOYMENT_ACTIVATED: 'workflow.deployment_activated',
+ WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted',
+ WORKFLOW_LOCKED: 'workflow.locked',
+ WORKFLOW_UNLOCKED: 'workflow.unlocked',
+ WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated',
+
+ // Workspaces
+ WORKSPACE_CREATED: 'workspace.created',
+ WORKSPACE_UPDATED: 'workspace.updated',
+ WORKSPACE_DELETED: 'workspace.deleted',
+ WORKSPACE_DUPLICATED: 'workspace.duplicated',
+} as const
+
+export type AuditActionType = (typeof AuditAction)[keyof typeof AuditAction]
+
+/**
+ * All resource types that can appear in audit log entries.
+ */
+export const AuditResourceType = {
+ API_KEY: 'api_key',
+ BILLING: 'billing',
+ BYOK_KEY: 'byok_key',
+ CHAT: 'chat',
+ CONNECTOR: 'connector',
+ CREDENTIAL: 'credential',
+ CREDENTIAL_SET: 'credential_set',
+ CUSTOM_TOOL: 'custom_tool',
+ DOCUMENT: 'document',
+ ENVIRONMENT: 'environment',
+ FILE: 'file',
+ FOLDER: 'folder',
+ FORM: 'form',
+ KNOWLEDGE_BASE: 'knowledge_base',
+ MCP_SERVER: 'mcp_server',
+ NOTIFICATION: 'notification',
+ OAUTH: 'oauth',
+ ORGANIZATION: 'organization',
+ PASSWORD: 'password',
+ PERMISSION_GROUP: 'permission_group',
+ SCHEDULE: 'schedule',
+ SKILL: 'skill',
+ TABLE: 'table',
+ TEMPLATE: 'template',
+ WEBHOOK: 'webhook',
+ WORKFLOW: 'workflow',
+ WORKSPACE: 'workspace',
+} as const
+
+export type AuditResourceTypeValue = (typeof AuditResourceType)[keyof typeof AuditResourceType]
diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts
index 98e48bd7803..b08d15d7431 100644
--- a/apps/sim/lib/auth/auth.ts
+++ b/apps/sim/lib/auth/auth.ts
@@ -707,7 +707,8 @@ export const auth = betterAuth({
actorEmail: resetUser.email,
action: AuditAction.PASSWORD_RESET,
resourceType: AuditResourceType.PASSWORD,
- description: 'Password reset completed',
+ resourceId: resetUser.id,
+ description: `Password reset completed for ${resetUser.email}`,
})
},
},
diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts
index eb89c876cee..f75d6d7fbca 100644
--- a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts
+++ b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts
@@ -261,6 +261,7 @@ export async function executeDeployMcp(
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
description: `Undeployed workflow "${workflowId}" from MCP server`,
+ metadata: { workflowId, source: 'copilot' },
})
return {
@@ -324,6 +325,7 @@ export async function executeDeployMcp(
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
description: `Updated MCP tool "${toolName}" on server`,
+ metadata: { workflowId, toolName, source: 'copilot' },
})
return {
@@ -353,6 +355,7 @@ export async function executeDeployMcp(
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
description: `Deployed workflow as MCP tool "${toolName}"`,
+ metadata: { workflowId, toolName, toolId, source: 'copilot' },
})
return {
diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts
index 233b1cbfe25..00ecfce4dce 100644
--- a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts
+++ b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts
@@ -255,6 +255,11 @@ export async function executeCreateWorkspaceMcpServer(
resourceId: serverId,
resourceName: name,
description: `Created MCP server "${name}"`,
+ metadata: {
+ isPublic: params.isPublic ?? false,
+ toolCount: addedTools.length,
+ source: 'copilot',
+ },
})
return { success: true, output: { server, addedTools } }
@@ -314,6 +319,10 @@ export async function executeUpdateWorkspaceMcpServer(
resourceType: AuditResourceType.MCP_SERVER,
resourceId: params.serverId,
description: `Updated MCP server`,
+ metadata: {
+ updatedFields: Object.keys(updates).filter((k) => k !== 'updatedAt'),
+ source: 'copilot',
+ },
})
return { success: true, output: { serverId, ...updates, updatedAt: undefined } }
@@ -357,7 +366,9 @@ export async function executeDeleteWorkspaceMcpServer(
action: AuditAction.MCP_SERVER_REMOVED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: params.serverId,
- description: `Deleted MCP server`,
+ resourceName: existing.name,
+ description: `Deleted MCP server "${existing.name}"`,
+ metadata: { source: 'copilot' },
})
return { success: true, output: { serverId, name: existing.name, deleted: true } }
diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts
index 5cbbcdd5730..ac48a677081 100644
--- a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts
+++ b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts
@@ -241,6 +241,7 @@ async function executeManageCustomTool(
resourceId: created?.id,
resourceName: title,
description: `Created custom tool "${title}"`,
+ metadata: { source: 'copilot' },
})
return {
@@ -299,6 +300,7 @@ async function executeManageCustomTool(
resourceId: params.toolId,
resourceName: title,
description: `Updated custom tool "${title}"`,
+ metadata: { source: 'copilot' },
})
return {
@@ -334,6 +336,7 @@ async function executeManageCustomTool(
resourceType: AuditResourceType.CUSTOM_TOOL,
resourceId: params.toolId,
description: 'Deleted custom tool',
+ metadata: { source: 'copilot' },
})
return {
@@ -502,6 +505,7 @@ async function executeManageMcpTool(
description: existing
? `Updated existing MCP server "${config.name}"`
: `Added MCP server "${config.name}"`,
+ metadata: { transport: config.transport, url: config.url, source: 'copilot' },
})
return {
@@ -563,7 +567,9 @@ async function executeManageMcpTool(
action: AuditAction.MCP_SERVER_UPDATED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: params.serverId,
+ resourceName: updated.name,
description: `Updated MCP server "${updated.name}"`,
+ metadata: { source: 'copilot' },
})
return {
@@ -607,7 +613,9 @@ async function executeManageMcpTool(
action: AuditAction.MCP_SERVER_REMOVED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: params.serverId,
+ resourceName: deleted.name,
description: `Deleted MCP server "${deleted.name}"`,
+ metadata: { source: 'copilot' },
})
return {
@@ -719,6 +727,7 @@ async function executeManageSkill(
resourceId: created?.id,
resourceName: params.name,
description: `Created skill "${params.name}"`,
+ metadata: { source: 'copilot' },
})
return {
@@ -773,6 +782,7 @@ async function executeManageSkill(
resourceId: params.skillId,
resourceName: updatedName,
description: `Updated skill "${updatedName}"`,
+ metadata: { source: 'copilot' },
})
return {
@@ -804,6 +814,7 @@ async function executeManageSkill(
resourceType: AuditResourceType.SKILL,
resourceId: params.skillId,
description: 'Deleted skill',
+ metadata: { source: 'copilot' },
})
return {
@@ -1055,7 +1066,9 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
action: AuditAction.CREDENTIAL_RENAMED,
resourceType: AuditResourceType.OAUTH,
resourceId: credentialId,
+ resourceName: displayName,
description: `Renamed credential to "${displayName}"`,
+ metadata: { source: 'copilot' },
})
return { success: true, output: { credentialId, displayName } }
}
@@ -1067,6 +1080,7 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
resourceType: AuditResourceType.OAUTH,
resourceId: credentialId,
description: `Deleted credential`,
+ metadata: { source: 'copilot' },
})
return { success: true, output: { credentialId, deleted: true } }
}
diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts
index c2a378cb768..615fcdee647 100644
--- a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts
+++ b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts
@@ -141,6 +141,7 @@ export async function executeCreateWorkflow(
resourceId: result.workflowId,
resourceName: name,
description: `Created workflow "${name}"`,
+ metadata: { folderId, source: 'copilot' },
})
try {
@@ -216,6 +217,7 @@ export async function executeCreateFolder(
resourceId: result.folderId,
resourceName: name,
description: `Created folder "${name}"`,
+ metadata: { parentId, source: 'copilot' },
})
return { success: true, output: result }
@@ -372,6 +374,7 @@ export async function executeSetGlobalWorkflowVariables(
resourceType: AuditResourceType.WORKFLOW,
resourceId: workflowId,
description: `Updated workflow variables`,
+ metadata: { operationCount: operations.length, source: 'copilot' },
})
return { success: true, output: { updated: Object.values(byName).length } }
@@ -536,7 +539,10 @@ export async function executeGenerateApiKey(
actorId: context.userId,
action: AuditAction.API_KEY_CREATED,
resourceType: AuditResourceType.API_KEY,
- description: `Generated API key for workspace`,
+ resourceId: newKey.id,
+ resourceName: name,
+ description: `Generated API key "${name}" for workspace`,
+ metadata: { source: 'copilot' },
})
return {
diff --git a/apps/sim/lib/workflows/orchestration/chat-deploy.ts b/apps/sim/lib/workflows/orchestration/chat-deploy.ts
index 1a00e325d29..ada57d50641 100644
--- a/apps/sim/lib/workflows/orchestration/chat-deploy.ts
+++ b/apps/sim/lib/workflows/orchestration/chat-deploy.ts
@@ -155,7 +155,19 @@ export async function performChatDeploy(
resourceId: chatId,
resourceName: title,
description: `Deployed chat "${title}"`,
- metadata: { workflowId, identifier, authType },
+ metadata: {
+ workflowId,
+ identifier,
+ authType,
+ chatUrl,
+ isUpdate: !!existingDeployment,
+ hasOutputConfigs: outputConfigs.length > 0,
+ hasCustomizations: !!(
+ params.customizations?.primaryColor ||
+ params.customizations?.welcomeMessage ||
+ params.customizations?.imageUrl
+ ),
+ },
})
return { success: true, chatId, chatUrl }
@@ -200,6 +212,11 @@ export async function performChatUndeploy(
resourceId: chatId,
resourceName: chatRecord.title || chatId,
description: `Deleted chat deployment "${chatRecord.title || chatId}"`,
+ metadata: {
+ workflowId: chatRecord.workflowId || undefined,
+ identifier: chatRecord.identifier || undefined,
+ authType: chatRecord.authType || undefined,
+ },
})
return { success: true }
diff --git a/apps/sim/lib/workflows/orchestration/deploy.ts b/apps/sim/lib/workflows/orchestration/deploy.ts
index 5e8863ccb0b..d8709e47d50 100644
--- a/apps/sim/lib/workflows/orchestration/deploy.ts
+++ b/apps/sim/lib/workflows/orchestration/deploy.ts
@@ -209,7 +209,12 @@ export async function performFullDeploy(
resourceId: workflowId,
resourceName: (workflowData.name as string) || undefined,
description: `Deployed workflow "${(workflowData.name as string) || workflowId}"`,
- metadata: { version: deploymentVersionId },
+ metadata: {
+ deploymentVersionId,
+ version: deployResult.version,
+ previousVersionId: previousVersionId || undefined,
+ triggerWarnings: triggerSaveResult.warnings?.length ? triggerSaveResult.warnings : undefined,
+ },
request,
})
@@ -473,7 +478,12 @@ export async function performActivateVersion(
resourceType: AuditResourceType.WORKFLOW,
resourceId: workflowId,
description: `Activated deployment version ${version}`,
- metadata: { version },
+ resourceName: (workflow.name as string) || undefined,
+ metadata: {
+ version,
+ deploymentVersionId: versionRow.id,
+ previousVersionId: previousVersionId || undefined,
+ },
})
return {
diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts
index 0b4a46d9deb..a36f182738c 100644
--- a/packages/testing/src/mocks/audit.mock.ts
+++ b/packages/testing/src/mocks/audit.mock.ts
@@ -18,10 +18,13 @@ export const auditMock = {
PERSONAL_API_KEY_CREATED: 'personal_api_key.created',
PERSONAL_API_KEY_REVOKED: 'personal_api_key.revoked',
BYOK_KEY_CREATED: 'byok_key.created',
+ BYOK_KEY_UPDATED: 'byok_key.updated',
BYOK_KEY_DELETED: 'byok_key.deleted',
CHAT_DEPLOYED: 'chat.deployed',
CHAT_UPDATED: 'chat.updated',
CHAT_DELETED: 'chat.deleted',
+ CREDENTIAL_CREATED: 'credential.created',
+ CREDENTIAL_UPDATED: 'credential.updated',
CREDENTIAL_DELETED: 'credential.deleted',
CREDENTIAL_RENAMED: 'credential.renamed',
CREDIT_PURCHASED: 'credit.purchased',
@@ -43,6 +46,7 @@ export const auditMock = {
DOCUMENT_UPDATED: 'document.updated',
DOCUMENT_DELETED: 'document.deleted',
ENVIRONMENT_UPDATED: 'environment.updated',
+ ENVIRONMENT_DELETED: 'environment.deleted',
FILE_UPLOADED: 'file.uploaded',
FILE_UPDATED: 'file.updated',
FILE_DELETED: 'file.deleted',
@@ -55,6 +59,7 @@ export const auditMock = {
FORM_UPDATED: 'form.updated',
FORM_DELETED: 'form.deleted',
INVITATION_ACCEPTED: 'invitation.accepted',
+ INVITATION_RESENT: 'invitation.resent',
INVITATION_REVOKED: 'invitation.revoked',
CONNECTOR_CREATED: 'connector.created',
CONNECTOR_UPDATED: 'connector.updated',
@@ -75,6 +80,7 @@ export const auditMock = {
NOTIFICATION_DELETED: 'notification.deleted',
OAUTH_DISCONNECTED: 'oauth.disconnected',
PASSWORD_RESET: 'password.reset',
+ PASSWORD_RESET_REQUESTED: 'password.reset_requested',
ORGANIZATION_CREATED: 'organization.created',
ORGANIZATION_UPDATED: 'organization.updated',
ORG_MEMBER_ADDED: 'org_member.added',
@@ -85,12 +91,15 @@ export const auditMock = {
ORG_INVITATION_REJECTED: 'org_invitation.rejected',
ORG_INVITATION_CANCELLED: 'org_invitation.cancelled',
ORG_INVITATION_REVOKED: 'org_invitation.revoked',
+ ORG_INVITATION_RESENT: 'org_invitation.resent',
PERMISSION_GROUP_CREATED: 'permission_group.created',
PERMISSION_GROUP_UPDATED: 'permission_group.updated',
PERMISSION_GROUP_DELETED: 'permission_group.deleted',
PERMISSION_GROUP_MEMBER_ADDED: 'permission_group_member.added',
PERMISSION_GROUP_MEMBER_REMOVED: 'permission_group_member.removed',
+ SCHEDULE_CREATED: 'schedule.created',
SCHEDULE_UPDATED: 'schedule.updated',
+ SCHEDULE_DELETED: 'schedule.deleted',
SKILL_CREATED: 'skill.created',
SKILL_UPDATED: 'skill.updated',
SKILL_DELETED: 'skill.deleted',
@@ -115,6 +124,7 @@ export const auditMock = {
WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted',
WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated',
WORKSPACE_CREATED: 'workspace.created',
+ WORKSPACE_UPDATED: 'workspace.updated',
WORKSPACE_DELETED: 'workspace.deleted',
WORKSPACE_DUPLICATED: 'workspace.duplicated',
},
@@ -124,6 +134,7 @@ export const auditMock = {
BYOK_KEY: 'byok_key',
CHAT: 'chat',
CONNECTOR: 'connector',
+ CREDENTIAL: 'credential',
CREDENTIAL_SET: 'credential_set',
CUSTOM_TOOL: 'custom_tool',
DOCUMENT: 'document',
From bc31710c1c3fb211924e167163ad1ff142619f3c Mon Sep 17 00:00:00 2001
From: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
Date: Sat, 11 Apr 2026 20:37:18 -0700
Subject: [PATCH 6/7] improvement(landing): rebrand to AI workspace, add auth
modal, harden PostHog tracking (#4116)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* improvement: seo, geo, signup, posthog
* fix(landing): address PR review issues and convention violations
- Fix auth modal race condition: show loading state instead of redirecting when provider status hasn't loaded yet
- Fix auth modal HTTP error caching: reject non-200 responses so they aren't permanently cached
- Replace with next/image in auth modal
- Use cn() instead of template literal class concatenation in hero, footer-cta
- Remove commented-out dead code in footer, landing, sitemap
- Remove unused arrow property from FooterItem interface
- Convert relative imports to absolute in integrations/[slug]/page
- Remove no-op sanitizedName variable in signup form
- Remove unnecessary async from llms-full.txt route
- Remove extraneous non-TSDoc comment in auth modal
Co-Authored-By: Claude Opus 4.6
* style(landing): apply linter formatting fixes
Co-Authored-By: Claude Opus 4.6
* fix(landing): second pass — fix remaining code quality issues
- auth-modal: add @sim/logger, log social sign-in errors instead of swallowing silently
- auth-modal: extract duplicated social button classes into SOCIAL_BTN constant
- auth-modal: remove unused isProduction from ProviderStatus interface
- auth-modal: memoize getBrandConfig() call
- footer: remove stale arrow destructuring left after interface cleanup, use cn() throughout
- footer-cta: replace inline styles on submit button with Tailwind classes via cn()
- footer-cta: replace caretColor inline style with caret-white utility
- templates: fix incorrect section value 'landing_preview' → 'templates' for PostHog tracking
- events: add 'templates' to landing_cta_clicked section union
- integrations: replace "canvas" with "workflow builder" per constitution rules
- llms-full: replace "canvas" terminology with "visual builder"/"workflow builder"
Co-Authored-By: Claude Opus 4.6
* fix(landing): point Mothership and Workflows footer links to docs root
These docs pages don't exist yet — link to docs.sim.ai until they are published.
Co-Authored-By: Claude Opus 4.6
* fix(landing): complete rebrand in blog fallback description
Remove "workflows" from the non-tagged blog meta description to
align with the AI workspace rebrand across the rest of the PR.
Co-Authored-By: Claude Opus 4.6
* fix(landing): strip isProduction from provider response and handle late-resolve redirect
- Destructure only githubAvailable/googleAvailable from getOAuthProviderStatus
so isProduction is not leaked to unauthenticated callers.
- Add useEffect to redirect away from the modal if provider status resolves
after the modal is already open and no social providers are configured.
Co-Authored-By: Claude Opus 4.6
* fix(landing): align auth modal with login/signup page logic
- Add SSO button when NEXT_PUBLIC_SSO_ENABLED is set
- Gate "Continue with email" behind EMAIL_PASSWORD_SIGNUP_ENABLED
- Expose registrationDisabled from /api/auth/providers and hide
the "Sign up" toggle when registration is disabled
- Simplify skip-modal logic: redirect to full page when no social
providers or SSO are available (hasModalContent)
Co-Authored-By: Claude Opus 4.6
* fix(landing): force login view when registration is disabled
When a CTA passes defaultView='signup' but registration is disabled,
the modal now opens in login mode instead of showing "Create free
account" with social buttons that would fail on the backend.
Co-Authored-By: Claude Opus 4.6
* lint
* fix(landing): correct signup view when registrationDisabled loads late
When the user opens the modal before providerStatus resolves and
registrationDisabled comes back true, the view was stuck on 'signup'.
Now the late-resolve useEffect also forces the view to 'login'.
Co-Authored-By: Claude Opus 4.6
* fix(landing): add click tracking to integration page CTAs
Create IntegrationCtaButton client component that wraps AuthModal
and fires trackLandingCta on click, matching the pattern used by
every other landing section CTA.
Co-Authored-By: Claude Opus 4.6
* fix(landing): prevent mobile auth modal from unmounting on open
Remove setMobileMenuOpen(false) from mobile AuthModal button onClick
handlers. Closing the mobile menu unmounts the AuthModal before it
can open. The modal overlay or page redirect makes the menu
irrelevant without needing to explicitly close it.
Co-Authored-By: Claude Opus 4.6
---------
Co-authored-by: Waleed Latif
Co-authored-by: Claude Opus 4.6
---
.claude/rules/constitution.md | 71 +++++
.cursor/rules/constitution.mdc | 76 ++++++
apps/docs/app/[lang]/[[...slug]]/page.tsx | 12 +-
apps/docs/app/[lang]/layout.tsx | 2 +-
apps/docs/app/layout.tsx | 22 +-
apps/docs/app/llms.txt/route.ts | 4 +-
apps/docs/components/structured-data.tsx | 10 +-
.../content/docs/en/getting-started/index.mdx | 14 +-
apps/docs/content/docs/en/index.mdx | 12 +-
.../content/docs/en/introduction/index.mdx | 22 +-
apps/docs/public/favicon/site.webmanifest | 4 +-
apps/docs/public/llms.txt | 32 ++-
apps/sim/app/(auth)/login/login-form.tsx | 7 +-
apps/sim/app/(auth)/signup/signup-form.tsx | 26 +-
apps/sim/app/(landing)/blog/layout.tsx | 2 +-
apps/sim/app/(landing)/blog/page.tsx | 4 +-
.../components/auth-modal/auth-modal.tsx | 250 ++++++++++++++++++
.../collaboration/collaboration.tsx | 81 +++---
.../components/features/features.tsx | 29 +-
.../components/footer/footer-cta.tsx | 48 ++--
.../(landing)/components/footer/footer.tsx | 33 +--
.../app/(landing)/components/hero/hero.tsx | 48 ++--
.../landing-preview-panel.tsx | 45 ++--
.../navbar/components/product-dropdown.tsx | 8 +-
.../(landing)/components/navbar/navbar.tsx | 117 ++++----
.../(landing)/components/pricing/pricing.tsx | 62 +++--
.../(landing)/components/structured-data.tsx | 39 ++-
.../components/templates/templates.tsx | 6 +
.../components/integration-cta-button.tsx | 26 ++
.../components/template-card-button.tsx | 2 +
.../(landing)/integrations/[slug]/page.tsx | 79 +++---
apps/sim/app/(landing)/integrations/page.tsx | 18 +-
apps/sim/app/(landing)/landing-analytics.tsx | 9 +-
apps/sim/app/(landing)/landing.tsx | 11 +-
apps/sim/app/(landing)/models/page.tsx | 9 +-
apps/sim/app/(landing)/models/utils.ts | 2 +-
apps/sim/app/(landing)/partners/page.tsx | 14 +-
apps/sim/app/api/auth/providers/route.ts | 14 +
apps/sim/app/llms-full.txt/route.ts | 14 +-
apps/sim/app/llms.txt/route.ts | 10 +-
apps/sim/app/manifest.ts | 7 +-
apps/sim/app/page.tsx | 22 +-
apps/sim/app/robots.ts | 10 +
apps/sim/app/sitemap.ts | 18 +-
apps/sim/ee/whitelabeling/metadata.ts | 19 +-
apps/sim/lib/blog/seo.ts | 2 +-
apps/sim/lib/posthog/events.ts | 29 +-
apps/sim/public/llms.txt | 45 +++-
48 files changed, 1026 insertions(+), 420 deletions(-)
create mode 100644 .claude/rules/constitution.md
create mode 100644 .cursor/rules/constitution.mdc
create mode 100644 apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx
create mode 100644 apps/sim/app/(landing)/integrations/[slug]/components/integration-cta-button.tsx
create mode 100644 apps/sim/app/api/auth/providers/route.ts
diff --git a/.claude/rules/constitution.md b/.claude/rules/constitution.md
new file mode 100644
index 00000000000..6881c060ee8
--- /dev/null
+++ b/.claude/rules/constitution.md
@@ -0,0 +1,71 @@
+# Sim — Language & Positioning
+
+When editing user-facing copy (landing pages, docs, metadata, marketing), follow these rules.
+
+## Identity
+
+Sim is the **AI workspace** where teams build and run AI agents. Not a workflow tool, not an agent framework, not an automation platform.
+
+**Short definition:** Sim is the open-source AI workspace where teams build, deploy, and manage AI agents.
+
+**Full definition:** Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code.
+
+## Audience
+
+**Primary:** Teams building AI agents for their organization — IT, operations, and technical teams who need governance, security, lifecycle management, and collaboration.
+
+**Secondary:** Individual builders and developers who care about speed, flexibility, and open source.
+
+## Required Language
+
+| Concept | Use | Never use |
+|---------|-----|-----------|
+| The product | "AI workspace" | "workflow tool", "automation platform", "agent framework" |
+| Building | "build agents", "create agents" | "create workflows" (unless describing the workflow module specifically) |
+| Visual builder | "workflow builder" or "visual builder" | "canvas", "graph editor" |
+| Mothership | "Mothership" (capitalized) | "chat", "AI assistant", "copilot" |
+| Deployment | "deploy", "ship" | "publish", "activate" |
+| Audience | "teams", "builders" | "users", "customers" (in marketing copy) |
+| What agents do | "automate real work" | "automate tasks", "automate workflows" |
+| Our advantage | "open-source AI workspace" | "open-source platform" |
+
+## Tone
+
+- **Direct.** Short sentences. Active voice. Lead with what it does.
+- **Concrete.** Name specific things — "Slack bots, compliance agents, data pipelines" — not abstractions.
+- **Confident, not loud.** No exclamation marks or superlatives.
+- **Simple.** If a 16-year-old can't understand the sentence, rewrite it.
+
+## Claim Hierarchy
+
+When describing Sim, always lead with the most differentiated claim:
+
+1. **What it is:** "The AI workspace for teams"
+2. **What you do:** "Build, deploy, and manage AI agents"
+3. **How:** "Visually, conversationally, or with code"
+4. **Scale:** "1,000+ integrations, every major LLM"
+5. **Trust:** "Open source. SOC2. Trusted by 100,000+ builders."
+
+## Module Descriptions
+
+| Module | One-liner |
+|--------|-----------|
+| **Mothership** | Your AI command center. Build and manage everything in natural language. |
+| **Workflows** | The visual builder. Connect blocks, models, and integrations into agent logic. |
+| **Knowledge Base** | Your agents' memory. Upload docs, sync sources, build vector databases. |
+| **Tables** | A database, built in. Store, query, and wire structured data into agent runs. |
+| **Files** | Upload, create, and share. One store for your team and every agent. |
+| **Logs** | Full visibility, every run. Trace execution block by block. |
+
+## What We Never Say
+
+- Never call Sim "just a workflow tool"
+- Never compare only on integration count — we win on AI-native capabilities
+- Never use "no-code" as the primary descriptor — say "visually, conversationally, or with code"
+- Never promise unshipped features
+- Never use jargon ("RAG", "vector database", "MCP") without plain-English explanation on public pages
+- Avoid "agentic workforce" as a primary term — use "AI agents"
+
+## Vision
+
+Sim becomes the default environment where teams build AI agents — not a tool you visit for one task, but a workspace you live in. Workflows are one module; Mothership is another. The workspace is the constant; the interface adapts.
diff --git a/.cursor/rules/constitution.mdc b/.cursor/rules/constitution.mdc
new file mode 100644
index 00000000000..94186db6e3a
--- /dev/null
+++ b/.cursor/rules/constitution.mdc
@@ -0,0 +1,76 @@
+---
+description: Sim product language, positioning, and tone guidelines
+globs: ["apps/sim/app/(landing)/**", "apps/sim/app/(home)/**", "apps/docs/**", "apps/sim/app/manifest.ts", "apps/sim/app/sitemap.ts", "apps/sim/app/robots.ts", "apps/sim/app/llms.txt/**", "apps/sim/app/llms-full.txt/**", "apps/sim/app/(landing)/**/structured-data*", "apps/docs/**/structured-data*", "**/metadata*", "**/seo*"]
+---
+
+# Sim — Language & Positioning
+
+When editing user-facing copy (landing pages, docs, metadata, marketing), follow these rules.
+
+## Identity
+
+Sim is the **AI workspace** where teams build and run AI agents. Not a workflow tool, not an agent framework, not an automation platform.
+
+**Short definition:** Sim is the open-source AI workspace where teams build, deploy, and manage AI agents.
+
+**Full definition:** Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code.
+
+## Audience
+
+**Primary:** Teams building AI agents for their organization — IT, operations, and technical teams who need governance, security, lifecycle management, and collaboration.
+
+**Secondary:** Individual builders and developers who care about speed, flexibility, and open source.
+
+## Required Language
+
+| Concept | Use | Never use |
+|---------|-----|-----------|
+| The product | "AI workspace" | "workflow tool", "automation platform", "agent framework" |
+| Building | "build agents", "create agents" | "create workflows" (unless describing the workflow module specifically) |
+| Visual builder | "workflow builder" or "visual builder" | "canvas", "graph editor" |
+| Mothership | "Mothership" (capitalized) | "chat", "AI assistant", "copilot" |
+| Deployment | "deploy", "ship" | "publish", "activate" |
+| Audience | "teams", "builders" | "users", "customers" (in marketing copy) |
+| What agents do | "automate real work" | "automate tasks", "automate workflows" |
+| Our advantage | "open-source AI workspace" | "open-source platform" |
+
+## Tone
+
+- **Direct.** Short sentences. Active voice. Lead with what it does.
+- **Concrete.** Name specific things — "Slack bots, compliance agents, data pipelines" — not abstractions.
+- **Confident, not loud.** No exclamation marks or superlatives.
+- **Simple.** If a 16-year-old can't understand the sentence, rewrite it.
+
+## Claim Hierarchy
+
+When describing Sim, always lead with the most differentiated claim:
+
+1. **What it is:** "The AI workspace for teams"
+2. **What you do:** "Build, deploy, and manage AI agents"
+3. **How:** "Visually, conversationally, or with code"
+4. **Scale:** "1,000+ integrations, every major LLM"
+5. **Trust:** "Open source. SOC2. Trusted by 100,000+ builders."
+
+## Module Descriptions
+
+| Module | One-liner |
+|--------|-----------|
+| **Mothership** | Your AI command center. Build and manage everything in natural language. |
+| **Workflows** | The visual builder. Connect blocks, models, and integrations into agent logic. |
+| **Knowledge Base** | Your agents' memory. Upload docs, sync sources, build vector databases. |
+| **Tables** | A database, built in. Store, query, and wire structured data into agent runs. |
+| **Files** | Upload, create, and share. One store for your team and every agent. |
+| **Logs** | Full visibility, every run. Trace execution block by block. |
+
+## What We Never Say
+
+- Never call Sim "just a workflow tool"
+- Never compare only on integration count — we win on AI-native capabilities
+- Never use "no-code" as the primary descriptor — say "visually, conversationally, or with code"
+- Never promise unshipped features
+- Never use jargon ("RAG", "vector database", "MCP") without plain-English explanation on public pages
+- Avoid "agentic workforce" as a primary term — use "AI agents"
+
+## Vision
+
+Sim becomes the default environment where teams build AI agents — not a tool you visit for one task, but a workspace you live in. Workflows are one module; Mothership is another. The workspace is the constant; the interface adapts.
diff --git a/apps/docs/app/[lang]/[[...slug]]/page.tsx b/apps/docs/app/[lang]/[[...slug]]/page.tsx
index 94b9531ec12..d01cd5d3598 100644
--- a/apps/docs/app/[lang]/[[...slug]]/page.tsx
+++ b/apps/docs/app/[lang]/[[...slug]]/page.tsx
@@ -280,12 +280,12 @@ export async function generateMetadata(props: {
title: data.title,
description:
data.description ||
- 'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
+ 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.',
keywords: [
'AI agents',
- 'agentic workforce',
- 'AI agent platform',
- 'agentic workflows',
+ 'AI workspace',
+ 'AI agent builder',
+ 'build AI agents',
'LLM orchestration',
'AI automation',
'knowledge base',
@@ -300,7 +300,7 @@ export async function generateMetadata(props: {
title: data.title,
description:
data.description ||
- 'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
+ 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.',
url: fullUrl,
siteName: 'Sim Documentation',
type: 'article',
@@ -322,7 +322,7 @@ export async function generateMetadata(props: {
title: data.title,
description:
data.description ||
- 'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
+ 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents.',
images: [ogImageUrl],
creator: '@simdotai',
site: '@simdotai',
diff --git a/apps/docs/app/[lang]/layout.tsx b/apps/docs/app/[lang]/layout.tsx
index 5f9ca80cdd1..c500f440cbb 100644
--- a/apps/docs/app/[lang]/layout.tsx
+++ b/apps/docs/app/[lang]/layout.tsx
@@ -66,7 +66,7 @@ export default async function Layout({ children, params }: LayoutProps) {
'@type': 'WebSite',
name: 'Sim Documentation',
description:
- 'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
+ 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.',
url: 'https://docs.sim.ai',
publisher: {
'@type': 'Organization',
diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx
index ae83961eabd..80332d4c353 100644
--- a/apps/docs/app/layout.tsx
+++ b/apps/docs/app/layout.tsx
@@ -14,29 +14,27 @@ export const viewport: Viewport = {
export const metadata = {
metadataBase: new URL('https://docs.sim.ai'),
title: {
- default: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
+ default: 'Sim Documentation — The AI Workspace for Teams',
template: '%s | Sim Docs',
},
description:
- 'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
+ 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.',
applicationName: 'Sim Docs',
generator: 'Next.js',
referrer: 'origin-when-cross-origin' as const,
keywords: [
+ 'AI workspace',
+ 'AI agent builder',
'AI agents',
- 'agentic workforce',
- 'AI agent platform',
+ 'build AI agents',
'open-source AI agents',
- 'agentic workflows',
'LLM orchestration',
'AI integrations',
'knowledge base',
'AI automation',
- 'workflow builder',
- 'AI workflow orchestration',
+ 'visual workflow builder',
'enterprise AI',
'AI agent deployment',
- 'intelligent automation',
'AI tools',
],
authors: [{ name: 'Sim Team', url: 'https://sim.ai' }],
@@ -65,9 +63,9 @@ export const metadata = {
alternateLocale: ['es_ES', 'fr_FR', 'de_DE', 'ja_JP', 'zh_CN'],
url: 'https://docs.sim.ai',
siteName: 'Sim Documentation',
- title: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
+ title: 'Sim Documentation — The AI Workspace for Teams',
description:
- 'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
+ 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.',
images: [
{
url: 'https://docs.sim.ai/api/og?title=Sim%20Documentation',
@@ -79,9 +77,9 @@ export const metadata = {
},
twitter: {
card: 'summary_large_image',
- title: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
+ title: 'Sim Documentation — The AI Workspace for Teams',
description:
- 'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
+ 'Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.',
creator: '@simdotai',
site: '@simdotai',
images: ['https://docs.sim.ai/api/og?title=Sim%20Documentation'],
diff --git a/apps/docs/app/llms.txt/route.ts b/apps/docs/app/llms.txt/route.ts
index 352f4cc9043..c47d08d6b9e 100644
--- a/apps/docs/app/llms.txt/route.ts
+++ b/apps/docs/app/llms.txt/route.ts
@@ -37,9 +37,9 @@ export async function GET() {
const manifest = `# Sim Documentation
-> The open-source platform to build AI agents and run your agentic workforce.
+> The open-source AI workspace where teams build, deploy, and manage AI agents.
-Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders.
+Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code. Trusted by over 100,000 builders.
## Documentation Overview
diff --git a/apps/docs/components/structured-data.tsx b/apps/docs/components/structured-data.tsx
index 0a77701f859..cf9b74c6c09 100644
--- a/apps/docs/components/structured-data.tsx
+++ b/apps/docs/components/structured-data.tsx
@@ -70,10 +70,11 @@ export function StructuredData({
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'Sim',
- applicationCategory: 'DeveloperApplication',
+ applicationCategory: 'BusinessApplication',
+ applicationSubCategory: 'AI Workspace',
operatingSystem: 'Any',
description:
- 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs.',
+ 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.',
url: baseUrl,
author: {
'@type': 'Organization',
@@ -84,8 +85,9 @@ export function StructuredData({
category: 'Developer Tools',
},
featureList: [
- 'AI agent creation',
- 'Agentic workflow orchestration',
+ 'AI workspace for teams',
+ 'Mothership — natural language agent creation',
+ 'Visual workflow builder',
'1,000+ integrations',
'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
'Knowledge base creation',
diff --git a/apps/docs/content/docs/en/getting-started/index.mdx b/apps/docs/content/docs/en/getting-started/index.mdx
index 0f25b32a621..f63be54314d 100644
--- a/apps/docs/content/docs/en/getting-started/index.mdx
+++ b/apps/docs/content/docs/en/getting-started/index.mdx
@@ -170,17 +170,17 @@ Build, test, and refine workflows quickly with immediate feedback
## Next Steps
-
- Discover API, Function, Condition, and other workflow blocks
+
+ Discover API, Function, Condition, and other blocks
- Connect 160+ services including Gmail, Slack, Notion, and more
+ Connect 1,000+ services including Gmail, Slack, Notion, and more
Write custom functions for advanced data processing
-
- Make your workflow accessible via REST API or webhooks
+
+ Make your agent accessible via REST API or webhooks
@@ -188,7 +188,7 @@ Build, test, and refine workflows quickly with immediate feedback
**Need detailed explanations?** Visit the [Blocks documentation](/blocks) for comprehensive guides on each component.
-**Looking for integrations?** Explore the [Tools documentation](/tools) to see all 160+ available integrations.
+**Looking for integrations?** Explore the [Tools documentation](/tools) to see all 1,000+ available integrations.
**Ready to go live?** Learn about [Execution and Deployment](/execution) to make your workflows production-ready.
@@ -199,5 +199,5 @@ Build, test, and refine workflows quickly with immediate feedback
{ question: "Can I use a different AI model instead of GPT-4o?", answer: "Yes. The Agent block supports models from OpenAI, Anthropic, Google, Groq, Cerebras, DeepSeek, Mistral, xAI, and more. You can select any available model from the dropdown. If you self-host, you can also use local models through Ollama." },
{ question: "Can I import workflows from other tools?", answer: "Sim does not currently support importing workflows from other automation platforms. However, you can use the Copilot feature to describe what you want in natural language and have it build the workflow for you, which is often faster than manual recreation." },
{ question: "What if my workflow does not produce the expected output?", answer: "Use the Chat panel to test iteratively and inspect outputs from each block. You can click the dropdown to view different block outputs and pinpoint where the issue is. The execution logs (accessible from the Logs tab) show detailed information about each step including token usage, costs, and any errors." },
- { question: "Where do I go after completing this tutorial?", answer: "Explore the Blocks documentation to learn about Condition, Router, Function, and API blocks. Browse the Tools section to discover 160+ integrations you can add to your agents. When you are ready to deploy, check the Execution docs for REST API, webhook, and scheduled trigger options." },
+ { question: "Where do I go after completing this tutorial?", answer: "Explore the Blocks documentation to learn about Condition, Router, Function, and API blocks. Browse the Tools section to discover 1,000+ integrations you can add to your agents. When you are ready to deploy, check the Execution docs for REST API, webhook, and scheduled trigger options." },
]} />
diff --git a/apps/docs/content/docs/en/index.mdx b/apps/docs/content/docs/en/index.mdx
index a327eb44c08..6ad3ec93550 100644
--- a/apps/docs/content/docs/en/index.mdx
+++ b/apps/docs/content/docs/en/index.mdx
@@ -6,7 +6,7 @@ import { Card, Cards } from 'fumadocs-ui/components/card'
# Sim Documentation
-Welcome to Sim, a visual workflow builder for AI applications. Build powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas.
+Welcome to Sim, the open-source AI workspace where teams build, deploy, and manage AI agents. Create agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API — connected to 1,000+ integrations and every major LLM.
## Quick Start
@@ -15,13 +15,13 @@ Welcome to Sim, a visual workflow builder for AI applications. Build powerful AI
Learn what you can build with Sim
- Create your first workflow in 10 minutes
+ Build your first agent in 10 minutes
-
+
Learn about the building blocks
- Explore 80+ built-in integrations
+ Explore 1,000+ integrations
@@ -35,10 +35,10 @@ Welcome to Sim, a visual workflow builder for AI applications. Build powerful AI
Work with workflow and environment variables
- Monitor workflow runs and manage costs
+ Monitor agent runs and manage costs
- Start workflows via API, webhooks, or schedules
+ Start agents via API, webhooks, or schedules
diff --git a/apps/docs/content/docs/en/introduction/index.mdx b/apps/docs/content/docs/en/introduction/index.mdx
index 1b6cdc9ecae..891667249f7 100644
--- a/apps/docs/content/docs/en/introduction/index.mdx
+++ b/apps/docs/content/docs/en/introduction/index.mdx
@@ -8,7 +8,7 @@ import { Image } from '@/components/ui/image'
import { Video } from '@/components/ui/video'
import { FAQ } from '@/components/ui/faq'
-Sim is an open-source visual workflow builder for building and deploying AI agent workflows. Design intelligent automation systems using a no-code interface—connect AI models, databases, APIs, and business tools through an intuitive drag-and-drop canvas. Whether you're building chatbots, automating business processes, or orchestrating complex data pipelines, Sim provides the tools to bring your AI workflows to life.
+Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Create agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API. Connect AI models, databases, APIs, and 1,000+ business tools to build agents that automate real work — from chatbots and compliance agents to data pipelines and ITSM automation.
- Create your first workflow in 10 minutes
+ Build your first agent in 10 minutes
-
+
Learn about the building blocks
- Explore 160+ built-in integrations
+ Explore 1,000+ integrations
Set up workspace roles and permissions
@@ -121,9 +121,9 @@ Ready to build your first AI workflow?
{ question: "Is Sim free to use?", answer: "Sim offers a free Community plan with 1,000 one-time credits to get started. Paid plans start at $25/month (Pro) with 5,000 credits and go up to $100/month (Max) with 20,000 credits. Annual billing is available at a 15% discount. You can also self-host Sim for free on your own infrastructure." },
{ question: "Is Sim open source?", answer: "Yes. Sim is open source under the Apache 2.0 license. The full source code is available on GitHub and you can self-host it, contribute to development, or modify it for your own needs. Enterprise features (SSO, access control) have a separate license that requires a subscription for production use." },
{ question: "Which AI models and providers are supported?", answer: "Sim supports 15+ providers including OpenAI, Anthropic, Google Gemini, Groq, Cerebras, DeepSeek, Mistral, xAI, and OpenRouter. You can also run local models through Ollama or VLLM at no API cost. Bring Your Own Key (BYOK) is supported so you can use your own API keys at base provider pricing with no markup." },
- { question: "Do I need coding experience to use Sim?", answer: "No. Sim is a no-code visual builder where you design workflows by dragging blocks onto a canvas and connecting them. For advanced use cases, the Function block lets you write custom JavaScript, but it is entirely optional." },
+ { question: "Do I need coding experience to use Sim?", answer: "No. Sim lets you build agents visually by dragging blocks onto a canvas and connecting them, or conversationally through Mothership using natural language. For advanced use cases, the Function block lets you write custom JavaScript, and the full API/SDK is available for programmatic access." },
{ question: "Can I self-host Sim?", answer: "Yes. Sim provides Docker Compose configurations for self-hosted deployments. The stack includes the Sim application, a PostgreSQL database with pgvector, and a realtime collaboration server. You can also integrate local AI models via Ollama for a fully offline setup." },
{ question: "Is there a limit on how many workflows I can create?", answer: "There is no limit on the number of workflows you can create on any plan. Usage limits apply to execution credits, rate limits, and file storage, which vary by plan tier." },
- { question: "What integrations are available?", answer: "Sim offers 160+ native integrations across categories including AI models, communication tools (Gmail, Slack, Teams, Telegram), productivity apps (Notion, Google Workspace, Airtable), development tools (GitHub, Jira, Linear), search services (Google Search, Perplexity, Exa), and databases (PostgreSQL, Supabase, Pinecone). For anything not built in, you can use the MCP (Model Context Protocol) support to connect custom services." },
- { question: "How does Sim compare to other workflow automation tools?", answer: "Sim is purpose-built for AI agent workflows rather than general task automation. It provides a visual canvas for orchestrating LLM-powered agents with built-in support for tool use, structured outputs, conditional branching, and real-time collaboration. The Copilot feature also lets you build and modify workflows using natural language." },
+ { question: "What integrations are available?", answer: "Sim offers 1,000+ native integrations across categories including AI models, communication tools (Gmail, Slack, Teams, Telegram), productivity apps (Notion, Google Workspace, Airtable), development tools (GitHub, Jira, Linear), search services (Google Search, Perplexity, Exa), and databases (PostgreSQL, Supabase, Pinecone). For anything not built in, you can use the MCP (Model Context Protocol) support to connect custom services." },
+ { question: "How does Sim compare to other AI agent builders?", answer: "Sim is an AI workspace — not just a workflow tool or an agent framework. It combines a visual workflow builder, Mothership for natural-language agent creation, knowledge bases, tables, and full observability in one environment. Teams build agents visually, conversationally, or with code, then deploy and manage them with enterprise governance, real-time collaboration, and staging-to-production workflows." },
]} />
diff --git a/apps/docs/public/favicon/site.webmanifest b/apps/docs/public/favicon/site.webmanifest
index 9bb874a0218..8905a552b58 100644
--- a/apps/docs/public/favicon/site.webmanifest
+++ b/apps/docs/public/favicon/site.webmanifest
@@ -1,7 +1,7 @@
{
- "name": "Sim Documentation — Build AI Agents & Run Your Agentic Workforce",
+ "name": "Sim Documentation — The AI Workspace for Teams",
"short_name": "Sim Docs",
- "description": "Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.",
+ "description": "Documentation for Sim — the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.",
"start_url": "/",
"scope": "/",
"icons": [
diff --git a/apps/docs/public/llms.txt b/apps/docs/public/llms.txt
index 8d03f2491c3..aad99e364ef 100644
--- a/apps/docs/public/llms.txt
+++ b/apps/docs/public/llms.txt
@@ -1,13 +1,15 @@
# Sim Documentation
-Sim is a visual workflow builder for AI applications that lets you build AI agent workflows visually. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required.
+Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Create agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API — connected to 1,000+ integrations and every major LLM.
## What is Sim?
-Sim provides a complete ecosystem for AI workflow automation including:
+Sim provides a complete AI workspace including:
+- Mothership — natural language agent creation and workspace management
- Visual workflow builder with drag-and-drop interface
-- AI agent creation and automation
-- 80+ built-in integrations (OpenAI, Slack, Gmail, GitHub, etc.)
+- 1,000+ built-in integrations (OpenAI, Anthropic, Slack, Gmail, GitHub, etc.)
+- Knowledge bases for retrieval-augmented generation
+- Built-in tables for structured data
- Real-time team collaboration
- Multiple deployment options (cloud-hosted or self-hosted)
- Custom integrations via MCP protocol
@@ -16,22 +18,22 @@ Sim provides a complete ecosystem for AI workflow automation including:
Here are the key areas covered in our documentation:
-/introduction - Getting started with Sim visual workflow builder
-/getting-started - Quick start guide for building your first workflow
-/blocks - Understanding workflow blocks (AI agents, APIs, functions)
-/tools - 80+ built-in integrations and tools
+/introduction - Getting started with Sim AI workspace
+/getting-started - Quick start guide for building your first agent
+/blocks - Understanding blocks (AI agents, APIs, functions)
+/tools - 1,000+ integrations and tools
/webhooks - Webhook triggers and handling
/mcp - Custom integrations via MCP protocol
/deployment - Cloud-hosted vs self-hosted deployment
/permissions - Team collaboration and workspace management
/collaboration - Real-time editing and team features
-/workflows - Building complex automation workflows
+/workflows - Building agent logic with the visual builder
## Technical Information
- Framework: Fumadocs (Next.js-based documentation platform)
- Content: MDX files with interactive examples
-- Languages: English (primary), French, Chinese
+- Languages: English (primary), Spanish, French, German, Japanese, Chinese
- Search: AI-powered search and assistance available
## Complete Documentation
@@ -40,14 +42,10 @@ For the full documentation with all pages, examples, and interactive features, v
## Additional Resources
-- GitHub repository with workflow examples
+- GitHub repository with agent examples
- Discord community for support and discussions
-- 80+ built-in integrations with detailed guides
+- 1,000+ built-in integrations with detailed guides
- MCP protocol documentation for custom integrations
- Self-hosting guides and Docker deployment
-For the complete documentation with interactive examples and visual workflow builder guides, visit https://docs.sim.ai
-
----
-
-Last updated: 2025-09-15
\ No newline at end of file
+For the complete documentation visit https://docs.sim.ai
diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx
index 8a43548acb4..27119bf1806 100644
--- a/apps/sim/app/(auth)/login/login-form.tsx
+++ b/apps/sim/app/(auth)/login/login-form.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useRef, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Eye, EyeOff, Loader2 } from 'lucide-react'
import Link from 'next/link'
@@ -20,6 +20,7 @@ import { validateCallbackUrl } from '@/lib/core/security/input-validation'
import { cn } from '@/lib/core/utils/cn'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
+import { captureClientEvent } from '@/lib/posthog/client'
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
@@ -113,6 +114,10 @@ export default function LoginPage({
: null
)
+ useEffect(() => {
+ captureClientEvent('login_page_viewed', {})
+ }, [])
+
const handleEmailChange = (e: React.ChangeEvent) => {
const newEmail = e.target.value
setEmail(newEmail)
diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx
index afb27cd729a..5d1b2d25ff6 100644
--- a/apps/sim/app/(auth)/signup/signup-form.tsx
+++ b/apps/sim/app/(auth)/signup/signup-form.tsx
@@ -12,7 +12,7 @@ import { client, useSession } from '@/lib/auth/auth-client'
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
-import { captureEvent } from '@/lib/posthog/client'
+import { captureClientEvent, captureEvent } from '@/lib/posthog/client'
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
@@ -71,15 +71,13 @@ const validateEmailField = (emailValue: string): string[] => {
return errors
}
-function SignupFormContent({
- githubAvailable,
- googleAvailable,
- isProduction,
-}: {
+interface SignupFormProps {
githubAvailable: boolean
googleAvailable: boolean
isProduction: boolean
-}) {
+}
+
+function SignupFormContent({ githubAvailable, googleAvailable, isProduction }: SignupFormProps) {
const router = useRouter()
const searchParams = useSearchParams()
const { refetch: refetchSession } = useSession()
@@ -87,8 +85,8 @@ function SignupFormContent({
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
- captureEvent(posthog, 'signup_page_viewed', {})
- }, [posthog])
+ captureClientEvent('signup_page_viewed', {})
+ }, [])
const [showPassword, setShowPassword] = useState(false)
const [password, setPassword] = useState('')
const [passwordErrors, setPasswordErrors] = useState([])
@@ -243,8 +241,6 @@ function SignupFormContent({
return
}
- const sanitizedName = trimmedName
-
let token: string | undefined
const widget = turnstileRef.current
if (turnstileSiteKey && widget) {
@@ -267,7 +263,7 @@ function SignupFormContent({
{
email: emailValue,
password: passwordValue,
- name: sanitizedName,
+ name: trimmedName,
},
{
headers: {
@@ -629,11 +625,7 @@ export default function SignupPage({
githubAvailable,
googleAvailable,
isProduction,
-}: {
- githubAvailable: boolean
- googleAvailable: boolean
- isProduction: boolean
-}) {
+}: SignupFormProps) {
return (
Loading...
}
diff --git a/apps/sim/app/(landing)/blog/layout.tsx b/apps/sim/app/(landing)/blog/layout.tsx
index 6d505132e37..512f41a32ee 100644
--- a/apps/sim/app/(landing)/blog/layout.tsx
+++ b/apps/sim/app/(landing)/blog/layout.tsx
@@ -10,7 +10,7 @@ export default async function StudioLayout({ children }: { children: React.React
name: 'Sim',
url: 'https://sim.ai',
description:
- 'Sim is an open-source platform for building, testing, and deploying AI agent workflows.',
+ 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents.',
logo: 'https://sim.ai/logo/primary/small.png',
sameAs: [
'https://x.com/simdotai',
diff --git a/apps/sim/app/(landing)/blog/page.tsx b/apps/sim/app/(landing)/blog/page.tsx
index 9bae1dd8aea..a7339cc76ad 100644
--- a/apps/sim/app/(landing)/blog/page.tsx
+++ b/apps/sim/app/(landing)/blog/page.tsx
@@ -19,8 +19,8 @@ export async function generateMetadata({
const title = titleParts.join(' — ')
const description = tag
- ? `Sim blog posts tagged "${tag}" — insights and guides for building AI agent workflows.`
- : 'Announcements, insights, and guides for building AI agent workflows.'
+ ? `Sim blog posts tagged "${tag}" — insights and guides for building AI agents.`
+ : 'Announcements, insights, and guides for building AI agents.'
const canonicalParams = new URLSearchParams()
if (tag) canonicalParams.set('tag', tag)
diff --git a/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx b/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx
new file mode 100644
index 00000000000..d7a213f2499
--- /dev/null
+++ b/apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx
@@ -0,0 +1,250 @@
+'use client'
+
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import { createLogger } from '@sim/logger'
+import { Loader2, X } from 'lucide-react'
+import Image from 'next/image'
+import { useRouter } from 'next/navigation'
+import { Modal, ModalClose, ModalContent, ModalTitle, ModalTrigger } from '@/components/emcn'
+import { GithubIcon, GoogleIcon } from '@/components/icons'
+import { client } from '@/lib/auth/auth-client'
+import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
+import { captureClientEvent } from '@/lib/posthog/client'
+import type { PostHogEventMap } from '@/lib/posthog/events'
+import { getBrandConfig } from '@/ee/whitelabeling'
+
+const logger = createLogger('AuthModal')
+
+type AuthView = 'login' | 'signup'
+
+interface AuthModalProps {
+ children: React.ReactNode
+ defaultView?: AuthView
+ source: PostHogEventMap['auth_modal_opened']['source']
+}
+
+interface ProviderStatus {
+ githubAvailable: boolean
+ googleAvailable: boolean
+ registrationDisabled: boolean
+}
+
+let fetchPromise: Promise | null = null
+
+const FALLBACK_STATUS: ProviderStatus = {
+ githubAvailable: false,
+ googleAvailable: false,
+ registrationDisabled: false,
+}
+
+const SOCIAL_BTN =
+ 'relative flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[var(--landing-border-strong)] text-[13.5px] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)] disabled:cursor-not-allowed disabled:opacity-50'
+
+function fetchProviderStatus(): Promise {
+ if (fetchPromise) return fetchPromise
+ fetchPromise = fetch('/api/auth/providers')
+ .then((r) => {
+ if (!r.ok) throw new Error(`HTTP ${r.status}`)
+ return r.json()
+ })
+ .then(({ githubAvailable, googleAvailable, registrationDisabled }: ProviderStatus) => ({
+ githubAvailable,
+ googleAvailable,
+ registrationDisabled,
+ }))
+ .catch(() => {
+ fetchPromise = null
+ return FALLBACK_STATUS
+ })
+ return fetchPromise
+}
+
+export function AuthModal({ children, defaultView = 'login', source }: AuthModalProps) {
+ const router = useRouter()
+ const [open, setOpen] = useState(false)
+ const [view, setView] = useState(defaultView)
+ const [providerStatus, setProviderStatus] = useState(null)
+ const [socialLoading, setSocialLoading] = useState<'github' | 'google' | null>(null)
+ const brand = useMemo(() => getBrandConfig(), [])
+
+ useEffect(() => {
+ fetchProviderStatus().then(setProviderStatus)
+ }, [])
+
+ const hasSocial = providerStatus?.githubAvailable || providerStatus?.googleAvailable
+ const ssoEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
+ const emailEnabled = !isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED'))
+ const hasModalContent = hasSocial || ssoEnabled
+
+ useEffect(() => {
+ if (!open || !providerStatus) return
+ if (!hasModalContent) {
+ setOpen(false)
+ router.push(defaultView === 'login' ? '/login' : '/signup')
+ return
+ }
+ if (providerStatus.registrationDisabled && view === 'signup') {
+ setView('login')
+ }
+ }, [open, providerStatus, hasModalContent, defaultView, router, view])
+
+ const handleOpenChange = useCallback(
+ (nextOpen: boolean) => {
+ if (nextOpen && providerStatus && !hasModalContent) {
+ router.push(defaultView === 'login' ? '/login' : '/signup')
+ return
+ }
+ setOpen(nextOpen)
+ if (nextOpen) {
+ const initialView =
+ defaultView === 'signup' && providerStatus?.registrationDisabled ? 'login' : defaultView
+ setView(initialView)
+ captureClientEvent('auth_modal_opened', { view: initialView, source })
+ }
+ },
+ [defaultView, hasModalContent, providerStatus, router, source]
+ )
+
+ const handleSocialLogin = useCallback(async (provider: 'github' | 'google') => {
+ setSocialLoading(provider)
+ try {
+ await client.signIn.social({ provider, callbackURL: '/workspace' })
+ } catch (error) {
+ logger.warn('Social sign-in did not complete', { provider, error })
+ } finally {
+ setSocialLoading(null)
+ }
+ }, [])
+
+ const handleSSOLogin = useCallback(() => {
+ setOpen(false)
+ router.push('/sso')
+ }, [router])
+
+ const handleEmailContinue = useCallback(() => {
+ setOpen(false)
+ router.push(view === 'login' ? '/login' : '/signup')
+ }, [router, view])
+
+ return (
+
+ {children}
+
+
+ {view === 'login' ? 'Log in' : 'Create account'}
+
+
+
+
+
+ Close
+
+
+ {!providerStatus ? (
+
+
+
+ ) : (
+ <>
+
+
+
+
+ Start building.
+
+
+ {view === 'login' ? 'Log in to continue' : 'Create free account'}
+
+
+
+
+
+ {providerStatus.googleAvailable && (
+ handleSocialLogin('google')}
+ disabled={!!socialLoading}
+ className={SOCIAL_BTN}
+ >
+
+
+ {socialLoading === 'google' ? 'Connecting...' : 'Continue with Google'}
+
+
+ )}
+ {providerStatus.githubAvailable && (
+ handleSocialLogin('github')}
+ disabled={!!socialLoading}
+ className={SOCIAL_BTN}
+ >
+
+
+ {socialLoading === 'github' ? 'Connecting...' : 'Continue with GitHub'}
+
+
+ )}
+ {ssoEnabled && (
+
+ Sign in with SSO
+
+ )}
+
+
+ {emailEnabled && (
+ <>
+
+
+
+ Continue with email
+
+ >
+ )}
+
+
+
+ {view === 'login' ? "Don't have an account? " : 'Already have an account? '}
+
+ {view === 'login' && providerStatus.registrationDisabled ? (
+ Registration is disabled
+ ) : (
+ setView(view === 'login' ? 'signup' : 'login')}
+ className='text-[var(--landing-text)] underline-offset-4 transition hover:text-white hover:underline'
+ >
+ {view === 'login' ? 'Sign up' : 'Sign in'}
+
+ )}
+
+ >
+ )}
+
+
+
+ )
+}
diff --git a/apps/sim/app/(landing)/components/collaboration/collaboration.tsx b/apps/sim/app/(landing)/components/collaboration/collaboration.tsx
index 5db2aad66c0..13200885ba7 100644
--- a/apps/sim/app/(landing)/components/collaboration/collaboration.tsx
+++ b/apps/sim/app/(landing)/components/collaboration/collaboration.tsx
@@ -4,6 +4,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { Badge } from '@/components/emcn'
+import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
+import { trackLandingCta } from '@/app/(landing)/landing-analytics'
interface DotGridProps {
className?: string
@@ -250,10 +252,10 @@ export default function Collaboration() {
- Sim supports real-time multiplayer collaboration. Teams can build AI agents together
- in a shared workspace with live cursors, presence indicators, and concurrent editing.
- Features include role-based access control, shared workflows, and team workspace
- management.
+ Sim supports real-time multiplayer collaboration. Teams build AI agents together in a
+ shared workspace with live cursors, presence indicators, and concurrent editing.
+ Features include role-based access control, shared agents and workflows, and team
+ workspace management.
@@ -261,45 +263,54 @@ export default function Collaboration() {
in real-time inside your workspace.
-
- Build together
-
+
+ trackLandingCta({
+ label: 'Build together',
+ section: 'collaboration',
+ destination: 'auth_modal',
+ })
+ }
>
-
-
-
-
+ xmlns='http://www.w3.org/2000/svg'
+ >
+
+
+
+
+
))}
- Design powerful workflows, connect your data, and monitor every run — all in one
- platform.
+ Build agents, connect your data, and monitor every run — all in one workspace.
@@ -265,12 +265,21 @@ export default function Features() {
{FEATURE_TABS[activeTab].description}
-
- {FEATURE_TABS[activeTab].cta}
-
+
+
+ trackLandingCta({
+ label: FEATURE_TABS[activeTab].cta,
+ section: 'features',
+ destination: 'auth_modal',
+ })
+ }
+ >
+ {FEATURE_TABS[activeTab].cta}
+
+
diff --git a/apps/sim/app/(landing)/components/footer/footer-cta.tsx b/apps/sim/app/(landing)/components/footer/footer-cta.tsx
index f9af4ac4bcc..9a06d79fafe 100644
--- a/apps/sim/app/(landing)/components/footer/footer-cta.tsx
+++ b/apps/sim/app/(landing)/components/footer/footer-cta.tsx
@@ -2,8 +2,9 @@
import { useCallback, useRef, useState } from 'react'
import { ArrowUp } from 'lucide-react'
-import Link from 'next/link'
+import { cn } from '@/lib/core/utils/cn'
import { captureClientEvent } from '@/lib/posthog/client'
+import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
import { useLandingSubmit } from '@/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
@@ -70,8 +71,8 @@ export function FooterCTA() {
aria-label='Describe what you want to build'
placeholder={animatedPlaceholder}
rows={2}
- className='m-0 box-border min-h-[48px] w-full resize-none border-0 bg-transparent px-1 py-1 font-body text-[var(--landing-text)] text-base leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[var(--landing-text-muted)] focus-visible:ring-0'
- style={{ caretColor: '#FFFFFF', maxHeight: `${MAX_HEIGHT}px` }}
+ className='m-0 box-border min-h-[48px] w-full resize-none border-0 bg-transparent px-1 py-1 font-body text-[var(--landing-text)] text-base leading-[24px] tracking-[-0.015em] caret-white outline-none placeholder:font-[380] placeholder:text-[var(--landing-text-muted)] focus-visible:ring-0'
+ style={{ maxHeight: `${MAX_HEIGHT}px` }}
/>
@@ -96,7 +96,10 @@ export function FooterCTA() {
href='https://docs.sim.ai'
target='_blank'
rel='noopener noreferrer'
- className={`${CTA_BUTTON} border-[var(--landing-border-strong)] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]`}
+ className={cn(
+ CTA_BUTTON,
+ 'border-[var(--landing-border-strong)] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]'
+ )}
onClick={() =>
trackLandingCta({
label: 'Docs',
@@ -107,15 +110,24 @@ export function FooterCTA() {
>
Docs
-
- trackLandingCta({ label: 'Get started', section: 'footer_cta', destination: '/signup' })
- }
- >
- Get started
-
+
+
+ trackLandingCta({
+ label: 'Get started',
+ section: 'footer_cta',
+ destination: 'auth_modal',
+ })
+ }
+ >
+ Get started
+
+
)
diff --git a/apps/sim/app/(landing)/components/footer/footer.tsx b/apps/sim/app/(landing)/components/footer/footer.tsx
index 6cd9c3f7f3e..a27a1cf30bf 100644
--- a/apps/sim/app/(landing)/components/footer/footer.tsx
+++ b/apps/sim/app/(landing)/components/footer/footer.tsx
@@ -1,5 +1,6 @@
import Image from 'next/image'
import Link from 'next/link'
+import { cn } from '@/lib/core/utils/cn'
import { FooterCTA } from '@/app/(landing)/components/footer/footer-cta'
const LINK_CLASS =
@@ -9,25 +10,24 @@ interface FooterItem {
label: string
href: string
external?: boolean
- arrow?: boolean
externalArrow?: boolean
}
const PRODUCT_LINKS: FooterItem[] = [
- { label: 'Self Hosting', href: 'https://docs.sim.ai/self-hosting', external: true },
- { label: 'MCP', href: 'https://docs.sim.ai/mcp', external: true },
+ { label: 'Mothership', href: 'https://docs.sim.ai', external: true },
+ { label: 'Workflows', href: 'https://docs.sim.ai', external: true },
{ label: 'Knowledge Base', href: 'https://docs.sim.ai/knowledgebase', external: true },
{ label: 'Tables', href: 'https://docs.sim.ai/tables', external: true },
+ { label: 'MCP', href: 'https://docs.sim.ai/mcp', external: true },
{ label: 'API', href: 'https://docs.sim.ai/api-reference/getting-started', external: true },
+ { label: 'Self Hosting', href: 'https://docs.sim.ai/self-hosting', external: true },
{ label: 'Status', href: 'https://status.sim.ai', external: true, externalArrow: true },
]
const RESOURCES_LINKS: FooterItem[] = [
{ label: 'Blog', href: '/blog' },
- // { label: 'Templates', href: '/templates' },
{ label: 'Docs', href: 'https://docs.sim.ai', external: true },
{ label: 'Models', href: '/models' },
- // { label: 'Academy', href: '/academy' },
{ label: 'Partners', href: '/partners' },
{ label: 'Careers', href: 'https://jobs.ashbyhq.com/sim', external: true, externalArrow: true },
{ label: 'Changelog', href: '/changelog' },
@@ -47,7 +47,7 @@ const BLOCK_LINKS: FooterItem[] = [
]
const INTEGRATION_LINKS: FooterItem[] = [
- { label: 'All Integrations', href: '/integrations', arrow: true },
+ { label: 'All Integrations', href: '/integrations' },
{ label: 'Confluence', href: 'https://docs.sim.ai/tools/confluence', external: true },
{ label: 'Slack', href: 'https://docs.sim.ai/tools/slack', external: true },
{ label: 'GitHub', href: 'https://docs.sim.ai/tools/github', external: true },
@@ -93,7 +93,7 @@ const LEGAL_LINKS: FooterItem[] = [
function ChevronArrow({ external }: { external?: boolean }) {
return (
{title}
- {items.map(({ label, href, external, arrow, externalArrow }) =>
+ {items.map(({ label, href, external, externalArrow }) =>
external ? (
{label}
{externalArrow && }
) : (
-
+
{label}
- {arrow &&
}
)
)}
@@ -162,7 +160,10 @@ export default function Footer({ hideCTA }: FooterProps) {
return (
{!hideCTA && }
diff --git a/apps/sim/app/(landing)/components/hero/hero.tsx b/apps/sim/app/(landing)/components/hero/hero.tsx
index 7098c4abf71..217dd210edf 100644
--- a/apps/sim/app/(landing)/components/hero/hero.tsx
+++ b/apps/sim/app/(landing)/components/hero/hero.tsx
@@ -1,7 +1,8 @@
'use client'
import dynamic from 'next/dynamic'
-import Link from 'next/link'
+import { cn } from '@/lib/core/utils/cn'
+import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
import { DemoRequestModal } from '@/app/(landing)/components/demo-request/demo-request-modal'
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
@@ -16,7 +17,6 @@ const LandingPreview = dynamic(
}
)
-/** Shared base classes for CTA link buttons — matches Deploy/Run button styling in the preview panel. */
const CTA_BASE =
'inline-flex items-center h-[32px] rounded-[5px] border px-2.5 font-[430] font-season text-sm'
@@ -30,11 +30,11 @@ export default function Hero() {
className='relative flex flex-col items-center overflow-hidden bg-[var(--landing-bg)] pt-[60px] lg:pt-[100px]'
>
- Sim is an open-source AI agent platform. Sim lets teams build AI agents and run an agentic
- workforce by connecting 1,000+ integrations and LLMs — including OpenAI, Anthropic Claude,
- Google Gemini, Mistral, and xAI Grok — to deploy and orchestrate agentic workflows. Users
- create agents, workflows, knowledge bases, tables, and docs. Sim is trusted by over 100,000
- builders at startups and Fortune 500 companies. Sim is SOC2 compliant.
+ Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect
+ 1,000+ integrations and every major LLM — including OpenAI, Anthropic Claude, Google Gemini,
+ Mistral, and xAI Grok — to create agents that automate real work. Build agents visually with
+ the workflow builder, conversationally through Mothership, or programmatically with the API.
+ Trusted by over 100,000 builders at startups and Fortune 500 companies. SOC2 compliant.
@@ -56,7 +56,10 @@ export default function Hero() {
trackLandingCta({ label: 'Get a demo', section: 'hero', destination: 'demo_modal' })
@@ -65,16 +68,25 @@ export default function Hero() {
Get a demo
-
- trackLandingCta({ label: 'Get started', section: 'hero', destination: '/signup' })
- }
- >
- Get started
-
+
+
+ trackLandingCta({
+ label: 'Get started',
+ section: 'hero',
+ destination: 'auth_modal',
+ })
+ }
+ >
+ Get started
+
+
diff --git a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel.tsx b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel.tsx
index ef5929963e7..6e7ba497aeb 100644
--- a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel.tsx
+++ b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel.tsx
@@ -3,13 +3,13 @@
import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { ArrowUp } from 'lucide-react'
-import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { createPortal } from 'react-dom'
import { Blimp, BubbleChatPreview, ChevronDown, MoreHorizontal, Play } from '@/components/emcn'
import { AgentIcon, HubspotIcon, OpenAIIcon, SalesforceIcon } from '@/components/icons'
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
import { captureClientEvent } from '@/lib/posthog/client'
+import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
import {
EASE_OUT,
type EditorPromptData,
@@ -19,6 +19,7 @@ import {
TYPE_INTERVAL_MS,
TYPE_START_BUFFER_MS,
} from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
+import { trackLandingCta } from '@/app/(landing)/landing-analytics'
type PanelTab = 'copilot' | 'editor'
@@ -44,6 +45,11 @@ export function useLandingSubmit() {
const trimmed = text.trim()
if (!trimmed) return
LandingPromptStorage.store(trimmed)
+ trackLandingCta({
+ label: 'Prompt submit',
+ section: 'landing_preview',
+ destination: '/signup',
+ })
router.push('/signup')
},
[router]
@@ -175,20 +181,29 @@ export const LandingPreviewPanel = memo(function LandingPreviewPanel({
- setCursorPos({ x: e.clientX, y: e.clientY })}
- onMouseLeave={() => setCursorPos(null)}
- >
-
- Deploy
-
-
-
+
+ setCursorPos({ x: e.clientX, y: e.clientY })}
+ onMouseLeave={() => setCursorPos(null)}
+ onClick={() =>
+ trackLandingCta({
+ label: 'Deploy',
+ section: 'landing_preview',
+ destination: 'auth_modal',
+ })
+ }
+ >
+
+ Deploy
+
+
+
+
{cursorPos &&
createPortal(
- {PLATFORM.map((link) => (
+ {WORKSPACE.map((link) => (
))}
diff --git a/apps/sim/app/(landing)/components/navbar/navbar.tsx b/apps/sim/app/(landing)/components/navbar/navbar.tsx
index 8f595d69078..7e8d7342616 100644
--- a/apps/sim/app/(landing)/components/navbar/navbar.tsx
+++ b/apps/sim/app/(landing)/components/navbar/navbar.tsx
@@ -7,6 +7,7 @@ import { useSearchParams } from 'next/navigation'
import { GithubOutlineIcon } from '@/components/icons'
import { useSession } from '@/lib/auth/auth-client'
import { cn } from '@/lib/core/utils/cn'
+import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
import {
BlogDropdown,
type NavBlogPost,
@@ -29,6 +30,8 @@ interface NavLink {
const NAV_LINKS: NavLink[] = [
{ label: 'Docs', href: 'https://docs.sim.ai', external: true, icon: 'chevron', dropdown: 'docs' },
{ label: 'Blog', href: '/blog', icon: 'chevron', dropdown: 'blog' },
+ { label: 'Integrations', href: '/integrations' },
+ { label: 'Models', href: '/models' },
{ label: 'Pricing', href: '/#pricing' },
]
@@ -225,30 +228,38 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
) : (
<>
-
- trackLandingCta({ label: 'Log in', section: 'navbar', destination: '/login' })
- }
- >
- Log in
-
-
- trackLandingCta({
- label: 'Get started',
- section: 'navbar',
- destination: '/signup',
- })
- }
- >
- Get started
-
+
+
+ trackLandingCta({
+ label: 'Log in',
+ section: 'navbar',
+ destination: 'auth_modal',
+ })
+ }
+ >
+ Log in
+
+
+
+
+ trackLandingCta({
+ label: 'Get started',
+ section: 'navbar',
+ destination: 'auth_modal',
+ })
+ }
+ >
+ Get started
+
+
>
)}
@@ -335,32 +346,38 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
) : (
<>
- {
- trackLandingCta({ label: 'Log in', section: 'navbar', destination: '/login' })
- setMobileMenuOpen(false)
- }}
- aria-label='Log in'
- >
- Log in
-
- {
- trackLandingCta({
- label: 'Get started',
- section: 'navbar',
- destination: '/signup',
- })
- setMobileMenuOpen(false)
- }}
- aria-label='Get started with Sim'
- >
- Get started
-
+
+
+ trackLandingCta({
+ label: 'Log in',
+ section: 'navbar',
+ destination: 'auth_modal',
+ })
+ }
+ aria-label='Log in'
+ >
+ Log in
+
+
+
+
+ trackLandingCta({
+ label: 'Get started',
+ section: 'navbar',
+ destination: 'auth_modal',
+ })
+ }
+ aria-label='Get started with Sim'
+ >
+ Get started
+
+
>
)}
diff --git a/apps/sim/app/(landing)/components/pricing/pricing.tsx b/apps/sim/app/(landing)/components/pricing/pricing.tsx
index d4d0789467c..dac481c5908 100644
--- a/apps/sim/app/(landing)/components/pricing/pricing.tsx
+++ b/apps/sim/app/(landing)/components/pricing/pricing.tsx
@@ -1,7 +1,7 @@
'use client'
-import Link from 'next/link'
import { Badge } from '@/components/emcn'
+import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
import { DemoRequestModal } from '@/app/(landing)/components/demo-request/demo-request-modal'
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
@@ -37,7 +37,7 @@ const PRICING_TIERS: PricingTier[] = [
{
id: 'pro',
name: 'Pro',
- description: 'For professionals building production workflows',
+ description: 'For professionals deploying AI agents',
price: '$25',
billingPeriod: 'per month',
color: '#00F701',
@@ -55,7 +55,7 @@ const PRICING_TIERS: PricingTier[] = [
{
id: 'max',
name: 'Max',
- description: 'For power users and teams building at scale',
+ description: 'For teams building AI agents at scale',
price: '$100',
billingPeriod: 'per month',
color: '#FA4EDF',
@@ -163,33 +163,37 @@ function PricingCard({ tier }: PricingCardProps) {
) : isPro ? (
-
- trackLandingCta({
- label: tier.cta.label,
- section: 'pricing',
- destination: tier.cta.href || '/signup',
- })
- }
- >
- {tier.cta.label}
-
+
+
+ trackLandingCta({
+ label: tier.cta.label,
+ section: 'pricing',
+ destination: 'auth_modal',
+ })
+ }
+ >
+ {tier.cta.label}
+
+
) : (
-
- trackLandingCta({
- label: tier.cta.label,
- section: 'pricing',
- destination: tier.cta.href || '/signup',
- })
- }
- >
- {tier.cta.label}
-
+
+
+ trackLandingCta({
+ label: tier.cta.label,
+ section: 'pricing',
+ destination: 'auth_modal',
+ })
+ }
+ >
+ {tier.cta.label}
+
+
)}
diff --git a/apps/sim/app/(landing)/components/structured-data.tsx b/apps/sim/app/(landing)/components/structured-data.tsx
index 1fc0122650a..b03c4fb45e9 100644
--- a/apps/sim/app/(landing)/components/structured-data.tsx
+++ b/apps/sim/app/(landing)/components/structured-data.tsx
@@ -27,7 +27,7 @@ export default function StructuredData() {
name: 'Sim',
alternateName: 'Sim Studio',
description:
- 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
+ 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.',
url: 'https://sim.ai',
logo: {
'@type': 'ImageObject',
@@ -55,9 +55,9 @@ export default function StructuredData() {
'@type': 'WebSite',
'@id': 'https://sim.ai/#website',
url: 'https://sim.ai',
- name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
+ name: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents',
description:
- 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Join 100,000+ builders.',
+ 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM. Join 100,000+ builders.',
publisher: { '@id': 'https://sim.ai/#organization' },
inLanguage: 'en-US',
},
@@ -65,13 +65,13 @@ export default function StructuredData() {
'@type': 'WebPage',
'@id': 'https://sim.ai/#webpage',
url: 'https://sim.ai',
- name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
+ name: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents',
isPartOf: { '@id': 'https://sim.ai/#website' },
about: { '@id': 'https://sim.ai/#software' },
datePublished: '2024-01-01T00:00:00+00:00',
dateModified: new Date().toISOString(),
description:
- 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs.',
+ 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.',
breadcrumb: { '@id': 'https://sim.ai/#breadcrumb' },
inLanguage: 'en-US',
speakable: {
@@ -91,12 +91,14 @@ export default function StructuredData() {
'@type': 'WebApplication',
'@id': 'https://sim.ai/#software',
url: 'https://sim.ai',
- name: 'Sim — Build AI Agents & Run Your Agentic Workforce',
+ name: 'Sim — The AI Workspace',
description:
- 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 compliant.',
- applicationCategory: 'DeveloperApplication',
+ 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code. Trusted by over 100,000 builders. SOC2 compliant.',
+ applicationCategory: 'BusinessApplication',
+ applicationSubCategory: 'AI Workspace',
operatingSystem: 'Web',
browserRequirements: 'Requires a modern browser with JavaScript enabled',
+ installUrl: 'https://sim.ai/signup',
offers: [
{
'@type': 'Offer',
@@ -135,8 +137,9 @@ export default function StructuredData() {
},
],
featureList: [
- 'AI agent creation',
- 'Agentic workflow orchestration',
+ 'AI workspace for teams',
+ 'Mothership — natural language agent creation',
+ 'Visual workflow builder',
'1,000+ integrations',
'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
'Knowledge base creation',
@@ -176,19 +179,27 @@ export default function StructuredData() {
codeRepository: 'https://github.com/simstudioai/sim',
programmingLanguage: ['TypeScript', 'Python'],
runtimePlatform: 'Node.js',
- license: 'https://opensource.org/licenses/AGPL-3.0',
+ license: 'https://opensource.org/licenses/Apache-2.0',
isPartOf: { '@id': 'https://sim.ai/#software' },
},
{
'@type': 'FAQPage',
'@id': 'https://sim.ai/#faq',
mainEntity: [
+ {
+ '@type': 'Question',
+ name: 'What is the best AI agent builder?',
+ acceptedAnswer: {
+ '@type': 'Answer',
+ text: 'Sim is the open-source AI workspace trusted by over 100,000 builders for creating, deploying, and managing AI agents. Build agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API. Sim connects to 1,000+ integrations and all major LLMs (OpenAI, Anthropic, Google, xAI, Mistral), and includes knowledge bases, tables, real-time collaboration, and enterprise governance. Free tier available. SOC2 compliant. Self-hostable.',
+ },
+ },
{
'@type': 'Question',
name: 'What is Sim?',
acceptedAnswer: {
'@type': 'Answer',
- text: 'Sim is the open-source platform to build AI agents and run your agentic workforce. Teams connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 compliant.',
+ text: 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code. The workspace includes Mothership for natural-language creation, a visual workflow builder, knowledge bases, tables, and full observability. Trusted by over 100,000 builders. SOC2 compliant.',
},
},
{
@@ -212,7 +223,7 @@ export default function StructuredData() {
name: 'Do I need coding skills to use Sim?',
acceptedAnswer: {
'@type': 'Answer',
- text: 'No coding skills are required. Sim provides a visual interface for building AI agents and agentic workflows. Developers can also use custom functions, the API, and the CLI/SDK for advanced use cases.',
+ text: 'No coding skills are required. Sim provides multiple ways to build agents: a visual workflow builder for drag-and-drop creation, Mothership for building in natural language, and templates for common use cases. Developers can also use custom functions, the API, and the CLI/SDK for advanced use cases.',
},
},
{
@@ -228,7 +239,7 @@ export default function StructuredData() {
name: 'Is Sim open source?',
acceptedAnswer: {
'@type': 'Answer',
- text: 'Yes. Sim is fully open source under the AGPL-3.0 license. The source code is available on GitHub at github.com/simstudioai/sim. You can self-host Sim or use the hosted version at sim.ai.',
+ text: 'Yes. Sim is fully open source under the Apache 2.0 license. The source code is available on GitHub at github.com/simstudioai/sim. You can self-host Sim or use the hosted version at sim.ai.',
},
},
{
diff --git a/apps/sim/app/(landing)/components/templates/templates.tsx b/apps/sim/app/(landing)/components/templates/templates.tsx
index ecb9ae7fd37..cb799237ae3 100644
--- a/apps/sim/app/(landing)/components/templates/templates.tsx
+++ b/apps/sim/app/(landing)/components/templates/templates.tsx
@@ -9,6 +9,7 @@ import { Badge, ChevronDown } from '@/components/emcn'
import { LandingWorkflowSeedStorage } from '@/lib/core/utils/browser-storage'
import { cn } from '@/lib/core/utils/cn'
import { TEMPLATE_WORKFLOWS } from '@/app/(landing)/components/templates/template-workflows'
+import { trackLandingCta } from '@/app/(landing)/landing-analytics'
const logger = createLogger('LandingTemplates')
@@ -297,6 +298,11 @@ export default function Templates() {
})
} finally {
setIsPreparingTemplate(false)
+ trackLandingCta({
+ label: activeWorkflow.name,
+ section: 'templates',
+ destination: '/signup',
+ })
router.push('/signup')
}
}, [
diff --git a/apps/sim/app/(landing)/integrations/[slug]/components/integration-cta-button.tsx b/apps/sim/app/(landing)/integrations/[slug]/components/integration-cta-button.tsx
new file mode 100644
index 00000000000..b746d7a5277
--- /dev/null
+++ b/apps/sim/app/(landing)/integrations/[slug]/components/integration-cta-button.tsx
@@ -0,0 +1,26 @@
+'use client'
+
+import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
+import { trackLandingCta } from '@/app/(landing)/landing-analytics'
+
+interface IntegrationCtaButtonProps {
+ children: React.ReactNode
+ className?: string
+ label: string
+}
+
+export function IntegrationCtaButton({ children, className, label }: IntegrationCtaButtonProps) {
+ return (
+
+
+ trackLandingCta({ label, section: 'integrations', destination: 'auth_modal' })
+ }
+ >
+ {children}
+
+
+ )
+}
diff --git a/apps/sim/app/(landing)/integrations/[slug]/components/template-card-button.tsx b/apps/sim/app/(landing)/integrations/[slug]/components/template-card-button.tsx
index 5fffa1121b6..4f2a91d7e73 100644
--- a/apps/sim/app/(landing)/integrations/[slug]/components/template-card-button.tsx
+++ b/apps/sim/app/(landing)/integrations/[slug]/components/template-card-button.tsx
@@ -3,6 +3,7 @@
import { useRouter } from 'next/navigation'
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
import { cn } from '@/lib/core/utils/cn'
+import { trackLandingCta } from '@/app/(landing)/landing-analytics'
interface TemplateCardButtonProps {
prompt: string
@@ -15,6 +16,7 @@ export function TemplateCardButton({ prompt, className, children }: TemplateCard
function handleClick() {
LandingPromptStorage.store(prompt)
+ trackLandingCta({ label: 'Template card', section: 'integrations', destination: '/signup' })
router.push('/signup')
}
diff --git a/apps/sim/app/(landing)/integrations/[slug]/page.tsx b/apps/sim/app/(landing)/integrations/[slug]/page.tsx
index 35290f9e711..e93bf9c73fe 100644
--- a/apps/sim/app/(landing)/integrations/[slug]/page.tsx
+++ b/apps/sim/app/(landing)/integrations/[slug]/page.tsx
@@ -3,13 +3,14 @@ import Image from 'next/image'
import Link from 'next/link'
import { notFound } from 'next/navigation'
import { getBaseUrl } from '@/lib/core/utils/urls'
+import { IntegrationCtaButton } from '@/app/(landing)/integrations/[slug]/components/integration-cta-button'
+import { IntegrationFAQ } from '@/app/(landing)/integrations/[slug]/components/integration-faq'
+import { TemplateCardButton } from '@/app/(landing)/integrations/[slug]/components/template-card-button'
+import { IntegrationIcon } from '@/app/(landing)/integrations/components/integration-icon'
+import { blockTypeToIconMap } from '@/app/(landing)/integrations/data/icon-mapping'
+import integrations from '@/app/(landing)/integrations/data/integrations.json'
+import type { AuthType, FAQItem, Integration } from '@/app/(landing)/integrations/data/types'
import { TEMPLATES } from '@/app/workspace/[workspaceId]/home/components/template-prompts/consts'
-import { IntegrationIcon } from '../components/integration-icon'
-import { blockTypeToIconMap } from '../data/icon-mapping'
-import integrations from '../data/integrations.json'
-import type { AuthType, FAQItem, Integration } from '../data/types'
-import { IntegrationFAQ } from './components/integration-faq'
-import { TemplateCardButton } from './components/template-card-button'
const allIntegrations = integrations as Integration[]
const INTEGRATION_COUNT = allIntegrations.length
@@ -85,18 +86,18 @@ function buildFAQs(integration: Integration): FAQItem[] {
const faqs: FAQItem[] = [
{
question: `What is Sim's ${name} integration?`,
- answer: `Sim's ${name} integration lets you build AI-powered workflows that automate tasks in ${name} without writing code. ${description} You can connect ${name} to hundreds of other services in the same workflow — from CRMs and spreadsheets to messaging tools and databases.`,
+ answer: `Sim's ${name} integration lets you build AI agents that automate tasks in ${name} without writing code. ${description} You can connect ${name} to hundreds of other services in the same agent — from CRMs and spreadsheets to messaging tools and databases.`,
},
{
question: `What can I automate with ${name} in Sim?`,
answer:
topOpNames.length > 0
? `With Sim you can: ${topOpNames.join('; ')}${operations.length > 5 ? `; and ${operations.length - 5} more tools` : ''}. Each action runs inside an AI agent block, so you can combine ${name} with LLM reasoning, conditional logic, and data from any other connected service.`
- : `Sim lets you automate ${name} workflows by connecting it to an AI agent that can read from it, write to it, and chain it together with other services — all driven by natural-language instructions instead of rigid rules.`,
+ : `Sim lets you automate ${name} by connecting it to an AI agent that can read from it, write to it, and chain it together with other services — all driven by natural-language instructions instead of rigid rules.`,
},
{
question: `How do I connect ${name} to Sim?`,
- answer: `Getting started takes under five minutes: (1) Create a free account at sim.ai. (2) Open a new workflow. (3) Drag a ${name} block onto the canvas. (4) ${authStep} (5) Choose the tool you want to use, wire it to the inputs you need, and click Run. Your automation is live.`,
+ answer: `Getting started takes under five minutes: (1) Create a free account at sim.ai. (2) Open your workspace and create an agent. (3) Drag a ${name} block onto the workflow builder. (4) ${authStep} (5) Choose the tool you want to use, wire it to the inputs you need, and click Run. Your agent is live.`,
},
{
question: `Can I use ${name} as a tool inside an AI agent in Sim?`,
@@ -106,19 +107,19 @@ function buildFAQs(integration: Integration): FAQItem[] {
? [
{
question: `How do I ${topOpNames[0].toLowerCase()} with ${name} in Sim?`,
- answer: `Add a ${name} block to your workflow and select "${topOpNames[0]}" as the tool. Fill in the required fields — you can reference outputs from earlier steps, such as text generated by an AI agent or data fetched from another integration. No code is required.`,
+ answer: `Add a ${name} block to your agent and select "${topOpNames[0]}" as the tool. Fill in the required fields — you can reference outputs from earlier steps, such as text generated by an AI agent or data fetched from another integration. No code is required.`,
},
]
: []),
...(triggers.length > 0
? [
{
- question: `How do I trigger a Sim workflow from ${name} automatically?`,
- answer: `Add a ${name} trigger block to your workflow and copy the generated webhook URL. Paste that URL into ${name}'s webhook settings and select the events you want to listen for (${triggers.map((t) => t.name).join(', ')}). From that point on, every matching event in ${name} instantly fires your workflow — no polling, no delay.`,
+ question: `How do I trigger a Sim agent from ${name} automatically?`,
+ answer: `Add a ${name} trigger block to your agent and copy the generated webhook URL. Paste that URL into ${name}'s webhook settings and select the events you want to listen for (${triggers.map((t) => t.name).join(', ')}). From that point on, every matching event in ${name} instantly runs your agent — no polling, no delay.`,
},
{
- question: `What data does Sim receive when a ${name} event triggers a workflow?`,
- answer: `When ${name} fires a webhook, Sim receives the full event payload that ${name} sends — typically the record or object that changed, along with metadata like the event type and timestamp. Inside your workflow, every field from that payload is available as a variable you can pass to AI agents, conditions, or other integrations.`,
+ question: `What data does Sim receive when a ${name} event triggers an agent?`,
+ answer: `When ${name} fires a webhook, Sim receives the full event payload that ${name} sends — typically the record or object that changed, along with metadata like the event type and timestamp. Inside your agent, every field from that payload is available as a variable you can pass to AI blocks, conditions, or other integrations.`,
},
]
: []),
@@ -156,7 +157,7 @@ export async function generateMetadata({
.slice(0, 3)
.map((o) => o.name)
.join(', ')
- const metaDesc = `Automate ${name} with AI-powered workflows on Sim. ${description.slice(0, 100).trimEnd()}. Free to start.`
+ const metaDesc = `Automate ${name} with AI agents in Sim. ${description.slice(0, 100).trimEnd()}. Free to start.`
return {
title: `${name} Integration`,
@@ -166,15 +167,15 @@ export async function generateMetadata({
`${name} integration`,
`automate ${name}`,
`connect ${name}`,
- `${name} workflow`,
+ `${name} AI agent`,
`${name} AI automation`,
...(opSample ? [`${name} ${opSample}`] : []),
- 'workflow automation',
- 'no-code automation',
- 'AI agent workflow',
+ 'AI workspace integrations',
+ 'AI agent integrations',
+ 'AI agent builder',
],
openGraph: {
- title: `${name} Integration — AI Workflow Automation | Sim`,
+ title: `${name} Integration | Sim AI Workspace`,
description: `Connect ${name} to ${INTEGRATION_COUNT - 1}+ tools using AI agents. ${description.slice(0, 100).trimEnd()}.`,
url: `${baseUrl}/integrations/${slug}`,
type: 'website',
@@ -190,7 +191,7 @@ export async function generateMetadata({
twitter: {
card: 'summary_large_image',
title: `${name} Integration | Sim`,
- description: `Automate ${name} with AI-powered workflows. Connect to ${INTEGRATION_COUNT - 1}+ tools. Free to start.`,
+ description: `Automate ${name} with AI agents in Sim. Connect to ${INTEGRATION_COUNT - 1}+ tools. Free to start.`,
images: [{ url: `${baseUrl}/opengraph-image.png`, alt: `${name} Integration — Sim` }],
},
alternates: { canonical: `${baseUrl}/integrations/${slug}` },
@@ -249,7 +250,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
'@context': 'https://schema.org',
'@type': 'HowTo',
name: `How to automate ${name} with Sim`,
- description: `Step-by-step guide to connecting ${name} to AI-powered workflows in Sim.`,
+ description: `Step-by-step guide to connecting ${name} to AI agents in Sim.`,
step: [
{
'@type': 'HowToStep',
@@ -261,13 +262,13 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
'@type': 'HowToStep',
position: 2,
name: `Add a ${name} block`,
- text: `Open a workflow, drag a ${name} block onto the canvas, and authenticate with your ${name} credentials.`,
+ text: `Open your workspace, drag a ${name} block onto the workflow builder, and authenticate with your ${name} credentials.`,
},
{
'@type': 'HowToStep',
position: 3,
name: 'Configure and run',
- text: `Choose the operation you want, connect it to an AI agent, and run your workflow. Automate anything in ${name} without code.`,
+ text: `Choose the operation you want, connect it to an AI agent, and deploy. Automate anything in ${name} without code.`,
},
],
}
@@ -366,12 +367,12 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
{/* CTAs */}
-
Start building free
-
+
- Connect a {name} webhook to Sim and your workflow fires the instant an event happens
- — no polling, no delay.
+ Connect a {name} webhook to Sim and your agent runs the instant an event happens —
+ no polling, no delay.
@@ -533,10 +534,10 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
id='templates-heading'
className='mb-2 text-[20px] text-white leading-[100%] tracking-[-0.02em]'
>
- Workflow templates
+ Agent templates
- Ready-to-use workflows featuring {name}. Click any to build it instantly.
+ Ready-to-use templates featuring {name}. Click any to build it instantly.
@@ -775,15 +776,15 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
Start automating {name} today
- Build your first AI workflow with {name} in minutes. Connect to every tool your team
- uses. Free to start — no credit card required.
+ Build your first AI agent with {name} in minutes. Connect to every tool your team uses.
+ Free to start — no credit card required.
-
Build for free
-
+
diff --git a/apps/sim/app/(landing)/integrations/page.tsx b/apps/sim/app/(landing)/integrations/page.tsx
index bdc67a537bd..60927489eeb 100644
--- a/apps/sim/app/(landing)/integrations/page.tsx
+++ b/apps/sim/app/(landing)/integrations/page.tsx
@@ -30,17 +30,17 @@ const featured = FEATURED_SLUGS.map((s) => bySlug.get(s)).filter(
export const metadata: Metadata = {
title: 'Integrations',
- description: `Connect ${INTEGRATION_COUNT}+ apps and services with Sim's AI workflow automation. Build intelligent pipelines with ${TOP_NAMES.join(', ')}, and more.`,
+ description: `Connect ${INTEGRATION_COUNT}+ apps and services in Sim's AI workspace. Build agents that automate real work with ${TOP_NAMES.join(', ')}, and more.`,
keywords: [
- 'workflow automation integrations',
- 'AI workflow automation',
- 'no-code automation',
+ 'AI workspace integrations',
+ 'AI agent integrations',
+ 'AI agent builder integrations',
...TOP_NAMES.flatMap((n) => [`${n} integration`, `${n} automation`]),
...allIntegrations.slice(0, 20).map((i) => `${i.name} automation`),
],
openGraph: {
- title: 'Integrations for AI Workflow Automation | Sim',
- description: `Connect ${INTEGRATION_COUNT}+ apps with Sim. Build AI-powered pipelines that link ${TOP_NAMES.join(', ')}, and every tool your team uses.`,
+ title: 'Integrations | Sim AI Workspace',
+ description: `Connect ${INTEGRATION_COUNT}+ apps in Sim's AI workspace. Build agents that link ${TOP_NAMES.join(', ')}, and every tool your team uses.`,
url: `${baseUrl}/integrations`,
type: 'website',
images: [
@@ -55,7 +55,7 @@ export const metadata: Metadata = {
twitter: {
card: 'summary_large_image',
title: 'Integrations | Sim',
- description: `Connect ${INTEGRATION_COUNT}+ apps with Sim's AI workflow automation.`,
+ description: `Connect ${INTEGRATION_COUNT}+ apps in Sim's AI workspace.`,
images: [
{ url: `${baseUrl}/opengraph-image.png`, alt: 'Sim Integrations for AI Workflow Automation' },
],
@@ -82,7 +82,7 @@ export default function IntegrationsPage() {
'@context': 'https://schema.org',
'@type': 'ItemList',
name: 'Sim AI Workflow Integrations',
- description: `Complete list of ${INTEGRATION_COUNT}+ integrations available in Sim for building AI-powered workflow automation.`,
+ description: `Complete list of ${INTEGRATION_COUNT}+ integrations available in Sim's AI workspace for building and deploying AI agents.`,
url: `${baseUrl}/integrations`,
numberOfItems: INTEGRATION_COUNT,
itemListElement: allIntegrations.map((integration, index) => ({
@@ -129,7 +129,7 @@ export default function IntegrationsPage() {
Integrations
- Connect every tool your team uses. Build AI-powered workflows that automate tasks across{' '}
+ Connect every tool your team uses. Build agents that automate real work across{' '}
{INTEGRATION_COUNT} apps and services.
diff --git a/apps/sim/app/(landing)/landing-analytics.tsx b/apps/sim/app/(landing)/landing-analytics.tsx
index d79e5faaa52..5a747b1306e 100644
--- a/apps/sim/app/(landing)/landing-analytics.tsx
+++ b/apps/sim/app/(landing)/landing-analytics.tsx
@@ -1,16 +1,13 @@
'use client'
import { useEffect } from 'react'
-import { usePostHog } from 'posthog-js/react'
-import { captureClientEvent, captureEvent } from '@/lib/posthog/client'
+import { captureClientEvent } from '@/lib/posthog/client'
import type { PostHogEventMap } from '@/lib/posthog/events'
export function LandingAnalytics() {
- const posthog = usePostHog()
-
useEffect(() => {
- captureEvent(posthog, 'landing_page_viewed', {})
- }, [posthog])
+ captureClientEvent('landing_page_viewed', {})
+ }, [])
return null
}
diff --git a/apps/sim/app/(landing)/landing.tsx b/apps/sim/app/(landing)/landing.tsx
index fd4b8eafc77..8250d2e1a30 100644
--- a/apps/sim/app/(landing)/landing.tsx
+++ b/apps/sim/app/(landing)/landing.tsx
@@ -3,7 +3,6 @@ import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
import { season } from '@/app/_styles/fonts/season/season'
import {
Collaboration,
- // Enterprise,
Features,
Footer,
Hero,
@@ -31,7 +30,7 @@ import { LandingAnalytics } from '@/app/(landing)/landing-analytics'
* - Section `id` attributes serve as fragment anchors for precise AI citations.
* - Content ordering prioritizes answer-first patterns: definition (Hero) ->
* examples (Templates) -> capabilities (Features) -> social proof (Collaboration) ->
- * enterprise (Enterprise) -> pricing (Pricing) -> testimonials (Testimonials).
+ * pricing (Pricing) -> testimonials (Testimonials).
*/
export default async function Landing() {
const blogPosts = await getNavBlogPosts()
@@ -53,16 +52,18 @@ export default async function Landing() {
-
+
- {/* */}
diff --git a/apps/sim/app/(landing)/models/page.tsx b/apps/sim/app/(landing)/models/page.tsx
index f98b9f73ceb..dd73b1ea1c2 100644
--- a/apps/sim/app/(landing)/models/page.tsx
+++ b/apps/sim/app/(landing)/models/page.tsx
@@ -42,17 +42,18 @@ const faqItems = [
'Tool use — also called function calling — lets an agent invoke external APIs, query databases, run code, or take any action you define. In Sim, all first-party models from OpenAI, Anthropic, Google, Mistral, Groq, Cerebras, and xAI support tool use. Look for the Tool Use capability tag on any model card in this directory to confirm support.',
},
{
- question: 'How do I add a model to a Sim agent workflow?',
+ question: 'How do I add a model to a Sim agent?',
answer:
- 'Open any workflow in Sim, add an Agent block, and select your provider and model from the model picker inside that block. Every model listed in this directory is available in the Agent block. Swapping models takes one click and does not affect the rest of your workflow, making it straightforward to test different models on the same task without rebuilding anything.',
+ 'Open Sim, add an Agent block, and select your provider and model from the model picker inside that block. Every model listed in this directory is available in the Agent block. Swapping models takes one click and does not affect the rest of your agent, making it straightforward to test different models on the same task without rebuilding anything.',
},
]
export const metadata: Metadata = {
title: 'AI Models Directory',
- description: `Browse ${TOTAL_MODELS}+ AI models across ${TOTAL_MODEL_PROVIDERS} providers. Compare pricing, context windows, and capabilities for OpenAI, Anthropic, Google, xAI, Mistral, Bedrock, Groq, and more.`,
+ description: `Browse and compare ${TOTAL_MODELS}+ AI models across ${TOTAL_MODEL_PROVIDERS} providers in Sim's AI workspace. Compare pricing, context windows, and capabilities — then use any model directly in your agents.`,
keywords: [
'AI models directory',
+ 'AI model comparison',
'LLM model list',
'model pricing',
'context window comparison',
@@ -185,7 +186,7 @@ export default function ModelsPage() {
id='models-heading'
className='text-balance text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]'
>
- Models
+ Compare AI Models
Browse {TOTAL_MODELS} AI models across {TOTAL_MODEL_PROVIDERS} providers. Compare
diff --git a/apps/sim/app/(landing)/models/utils.ts b/apps/sim/app/(landing)/models/utils.ts
index b0ff3260596..bb9a87e4ad3 100644
--- a/apps/sim/app/(landing)/models/utils.ts
+++ b/apps/sim/app/(landing)/models/utils.ts
@@ -695,7 +695,7 @@ export function buildModelFaqs(provider: CatalogProvider, model: CatalogModel):
{
question: `What is the context window for ${model.displayName}?`,
answer: model.contextWindow
- ? `${model.displayName} supports a context window of ${formatTokenCount(model.contextWindow)} tokens in Sim. In an agent workflow, this determines how much conversation history, tool outputs, and retrieved documents the model can hold in a single call.`
+ ? `${model.displayName} supports a context window of ${formatTokenCount(model.contextWindow)} tokens in Sim. In an agent, this determines how much conversation history, tool outputs, and retrieved documents the model can hold in a single call.`
: `A public context window value is not currently tracked for ${model.displayName}.`,
},
{
diff --git a/apps/sim/app/(landing)/partners/page.tsx b/apps/sim/app/(landing)/partners/page.tsx
index 851d9486556..ccdda2603ee 100644
--- a/apps/sim/app/(landing)/partners/page.tsx
+++ b/apps/sim/app/(landing)/partners/page.tsx
@@ -8,7 +8,7 @@ import Navbar from '@/app/(landing)/components/navbar/navbar'
export const metadata: Metadata = {
title: 'Partner Program',
description:
- 'Join the Sim partner program. Build, deploy, and sell AI workflow solutions. Earn your certification through Sim Academy.',
+ "Join the Sim partner program. Build, deploy, and sell AI agent solutions powered by Sim's AI workspace. Earn your certification through Sim Academy.",
metadataBase: new URL('https://sim.ai'),
openGraph: {
title: 'Partner Program | Sim',
@@ -22,7 +22,7 @@ const PARTNER_TIERS = [
name: 'Certified Partner',
badge: 'Entry',
color: '#3A3A3A',
- requirements: ['Complete Sim Academy certification', 'Deploy at least 1 live workflow'],
+ requirements: ['Complete Sim Academy certification', 'Deploy at least 1 live agent'],
perks: [
'Official partner badge',
'Listed in partner directory',
@@ -69,13 +69,13 @@ const HOW_IT_WORKS = [
step: '01',
title: 'Sign up & complete Sim Academy',
description:
- 'Create an account and work through the Sim Academy certification program. Learn to build, integrate, and deploy AI workflows through hands-on canvas exercises.',
+ 'Create an account and work through the Sim Academy certification program. Learn to build, integrate, and deploy AI agents through hands-on exercises.',
},
{
step: '02',
title: 'Build & deploy real solutions',
description:
- 'Put your skills to work. Build workflow automations for clients, integrate Sim into existing products, or create your own Sim-powered applications.',
+ 'Put your skills to work. Build AI agents for clients, integrate Sim into existing products, or create your own Sim-powered applications.',
},
{
step: '03',
@@ -119,7 +119,7 @@ const BENEFITS = [
icon: '📣',
title: 'Community',
description:
- 'Join a growing community of Sim builders. Share workflows, collaborate on solutions, and shape the product roadmap.',
+ 'Join a growing community of Sim builders. Share agents, collaborate on solutions, and shape the product roadmap.',
},
]
@@ -144,11 +144,11 @@ export default async function PartnersPage() {
Build the future
- of AI automation
+ of AI agents
Become a certified Sim partner. Complete Sim Academy, deploy real solutions, and earn
- recognition in the growing ecosystem of AI workflow builders.
+ recognition in the growing ecosystem of AI agent builders.
{/* TODO: Uncomment when academy is public */}
diff --git a/apps/sim/app/api/auth/providers/route.ts b/apps/sim/app/api/auth/providers/route.ts
new file mode 100644
index 00000000000..dadd3fcd083
--- /dev/null
+++ b/apps/sim/app/api/auth/providers/route.ts
@@ -0,0 +1,14 @@
+import { NextResponse } from 'next/server'
+import { isRegistrationDisabled } from '@/lib/core/config/feature-flags'
+import { getOAuthProviderStatus } from '@/app/(auth)/components/oauth-provider-checker'
+
+export const dynamic = 'force-dynamic'
+
+export async function GET() {
+ const { githubAvailable, googleAvailable } = await getOAuthProviderStatus()
+ return NextResponse.json({
+ githubAvailable,
+ googleAvailable,
+ registrationDisabled: isRegistrationDisabled,
+ })
+}
diff --git a/apps/sim/app/llms-full.txt/route.ts b/apps/sim/app/llms-full.txt/route.ts
index 915a2354143..b198f56aa6f 100644
--- a/apps/sim/app/llms-full.txt/route.ts
+++ b/apps/sim/app/llms-full.txt/route.ts
@@ -1,20 +1,20 @@
import { getBaseUrl } from '@/lib/core/utils/urls'
-export async function GET() {
+export function GET() {
const baseUrl = getBaseUrl()
- const llmsFullContent = `# Sim — Build AI Agents & Run Your Agentic Workforce
+ const llmsFullContent = `# Sim — The AI Workspace | Build, Deploy & Manage AI Agents
-> Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.
+> Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.
## Overview
-Sim lets teams create agents, workflows, knowledge bases, tables, and docs. Over 100,000 builders use Sim — from startups to Fortune 500 companies. Teams connect their tools and data, build agents that execute real workflows across systems, and manage them with full observability. SOC2 compliant.
+Sim is the AI workspace where teams create agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API. Over 100,000 builders use Sim — from startups to Fortune 500 companies. Teams connect their tools and data, build agents that automate real work across systems, and manage them with full observability. SOC2 compliant.
## Product Details
- **Product Name**: Sim
-- **Category**: AI Agent Platform / Agentic Workflow Orchestration
+- **Category**: AI Workspace / AI Agent Builder
- **Deployment**: Cloud (SaaS) and Self-hosted options
- **Pricing**: Free tier, Pro ($25/month, 6K credits), Max ($100/month, 25K credits), Team plans available, Enterprise (custom)
- **Compliance**: SOC2 Type II
@@ -123,7 +123,7 @@ Built-in table creation and management:
### Frontend
- Next.js 15 with App Router
-- React Flow for canvas visualization
+- React Flow for the visual builder
- Tailwind CSS for styling
- Zustand for state management
@@ -143,7 +143,7 @@ Built-in table creation and management:
1. **Sign Up**: Create a free account at ${baseUrl}
2. **Create Workspace**: Set up your first workspace
-3. **Build Workflow**: Drag blocks onto canvas and connect them
+3. **Build Workflow**: Drag blocks onto the workflow builder and connect them
4. **Configure Blocks**: Set up LLM providers, tools, and integrations
5. **Test**: Run the workflow manually to verify
6. **Deploy**: Set up triggers for automated execution
diff --git a/apps/sim/app/llms.txt/route.ts b/apps/sim/app/llms.txt/route.ts
index 89fbc5a67f4..0e6f7c31873 100644
--- a/apps/sim/app/llms.txt/route.ts
+++ b/apps/sim/app/llms.txt/route.ts
@@ -5,9 +5,9 @@ export function GET() {
const content = `# Sim
-> Sim is the open-source platform to build AI agents and run your agentic workforce. Connect integrations and LLMs to deploy and orchestrate agentic workflows.
+> Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.
-Sim lets teams create agents, workflows, knowledge bases, tables, and docs. It supports both product discovery pages and deeper technical documentation.
+Sim lets teams create agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API. The workspace includes knowledge bases, tables, files, and full observability.
## Preferred URLs
@@ -25,8 +25,8 @@ Sim lets teams create agents, workflows, knowledge bases, tables, and docs. It s
## Key Concepts
-- **Workspace**: Container for workflows, data sources, and executions
-- **Workflow**: Directed graph of blocks defining an agentic process
+- **Workspace**: The AI workspace — container for agents, workflows, data sources, and executions
+- **Workflow**: Visual builder — directed graph of blocks defining agent logic
- **Block**: Individual step such as an LLM call, tool call, HTTP request, or code execution
- **Trigger**: Event or schedule that initiates workflow execution
- **Execution**: A single run of a workflow with logs and outputs
@@ -34,8 +34,8 @@ Sim lets teams create agents, workflows, knowledge bases, tables, and docs. It s
## Capabilities
+- AI workspace for teams
- AI agent creation and deployment
-- Agentic workflow orchestration
- Integrations across business tools, databases, and communication platforms
- Multi-model LLM orchestration
- Knowledge bases and retrieval-augmented generation
diff --git a/apps/sim/app/manifest.ts b/apps/sim/app/manifest.ts
index 77c92d0c394..d66d2db1a17 100644
--- a/apps/sim/app/manifest.ts
+++ b/apps/sim/app/manifest.ts
@@ -5,10 +5,13 @@ export default function manifest(): MetadataRoute.Manifest {
const brand = getBrandConfig()
return {
- name: brand.name === 'Sim' ? 'Sim — Build AI Agents & Run Your Agentic Workforce' : brand.name,
+ name:
+ brand.name === 'Sim'
+ ? 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents'
+ : brand.name,
short_name: brand.name,
description:
- 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to orchestrate agentic workflows.',
+ 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM.',
start_url: '/',
scope: '/',
display: 'standalone',
diff --git a/apps/sim/app/page.tsx b/apps/sim/app/page.tsx
index c5e693a4cac..f746d2b3da6 100644
--- a/apps/sim/app/page.tsx
+++ b/apps/sim/app/page.tsx
@@ -9,12 +9,12 @@ const baseUrl = getBaseUrl()
export const metadata: Metadata = {
metadataBase: new URL(baseUrl),
title: {
- absolute: 'Sim — Build AI Agents & Run Your Agentic Workforce',
+ absolute: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents',
},
description:
- 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to orchestrate agentic workflows.',
+ 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code.',
keywords:
- 'AI agents, agentic workforce, open-source AI agent platform, agentic workflows, LLM orchestration, AI automation, knowledge base, workflow builder, AI integrations, SOC2 compliant, enterprise AI',
+ 'AI workspace, AI agent builder, AI agent workflow builder, build AI agents, visual workflow builder, open-source AI agent platform, AI agents, agentic workflows, LLM orchestration, AI automation, knowledge base, workflow builder, AI integrations, SOC2 compliant, enterprise AI',
authors: [{ name: 'Sim' }],
creator: 'Sim',
publisher: 'Sim',
@@ -24,9 +24,9 @@ export const metadata: Metadata = {
telephone: false,
},
openGraph: {
- title: 'Sim — Build AI Agents & Run Your Agentic Workforce',
+ title: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents',
description:
- 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Join over 100,000 builders.',
+ 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code.',
type: 'website',
url: baseUrl,
siteName: 'Sim',
@@ -36,7 +36,7 @@ export const metadata: Metadata = {
url: '/logo/426-240/primary/small.png',
width: 2130,
height: 1200,
- alt: 'Sim — Build AI Agents & Run Your Agentic Workforce',
+ alt: 'Sim — The AI Workspace for Teams',
type: 'image/png',
},
],
@@ -45,12 +45,12 @@ export const metadata: Metadata = {
card: 'summary_large_image',
site: '@simdotai',
creator: '@simdotai',
- title: 'Sim — Build AI Agents & Run Your Agentic Workforce',
+ title: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents',
description:
- 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to orchestrate agentic workflows.',
+ 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.',
images: {
url: '/logo/426-240/primary/small.png',
- alt: 'Sim — Build AI Agents & Run Your Agentic Workforce',
+ alt: 'Sim — The AI Workspace for Teams',
},
},
alternates: {
@@ -78,9 +78,9 @@ export const metadata: Metadata = {
referrer: 'origin-when-cross-origin',
other: {
'llm:content-type':
- 'AI agent platform, agentic workforce, agentic workflows, LLM orchestration',
+ 'AI workspace, AI agent builder, AI agent platform, agentic workflows, LLM orchestration',
'llm:use-cases':
- 'AI agents, agentic workforce, agentic workflows, knowledge bases, tables, document creation, email automation, Slack bots, data analysis, customer support, content generation',
+ 'build AI agents, AI workspace, visual workflow builder, natural language agent creation, knowledge bases, tables, document creation, email automation, Slack bots, data analysis, customer support, content generation',
'llm:integrations':
'OpenAI, Anthropic, Google AI, Mistral, xAI, Perplexity, Slack, Gmail, Discord, Notion, Airtable, Supabase',
'llm:pricing':
diff --git a/apps/sim/app/robots.ts b/apps/sim/app/robots.ts
index 710acc532c7..0a1a8929a33 100644
--- a/apps/sim/app/robots.ts
+++ b/apps/sim/app/robots.ts
@@ -119,6 +119,16 @@ export default function robots(): MetadataRoute.Robots {
allow: '/',
disallow: disallowedPaths,
},
+ {
+ userAgent: 'Grok-web-crawl',
+ allow: '/',
+ disallow: disallowedPaths,
+ },
+ {
+ userAgent: 'DeepSeek-AI',
+ allow: '/',
+ disallow: disallowedPaths,
+ },
],
sitemap: `${baseUrl}/sitemap.xml`,
host: baseUrl,
diff --git a/apps/sim/app/sitemap.ts b/apps/sim/app/sitemap.ts
index 0928460c001..32cf3f27be7 100644
--- a/apps/sim/app/sitemap.ts
+++ b/apps/sim/app/sitemap.ts
@@ -11,19 +11,27 @@ export default async function sitemap(): Promise
{
const integrationPages: MetadataRoute.Sitemap = integrations.map((integration) => ({
url: `${baseUrl}/integrations/${integration.slug}`,
lastModified: now,
+ changeFrequency: 'monthly',
+ priority: 0.6,
}))
const modelHubPages: MetadataRoute.Sitemap = [
{
url: `${baseUrl}/integrations`,
lastModified: now,
+ changeFrequency: 'weekly',
+ priority: 0.8,
},
{
url: `${baseUrl}/models`,
lastModified: now,
+ changeFrequency: 'weekly',
+ priority: 0.8,
},
{
url: `${baseUrl}/partners`,
lastModified: now,
+ changeFrequency: 'monthly',
+ priority: 0.5,
},
]
const providerPages: MetadataRoute.Sitemap = MODEL_PROVIDERS_WITH_CATALOGS.map((provider) => ({
@@ -31,16 +39,22 @@ export default async function sitemap(): Promise {
lastModified: new Date(
Math.max(...provider.models.map((model) => new Date(model.pricing.updatedAt).getTime()))
),
+ changeFrequency: 'weekly',
+ priority: 0.7,
}))
const modelPages: MetadataRoute.Sitemap = ALL_CATALOG_MODELS.map((model) => ({
url: `${baseUrl}${model.href}`,
lastModified: new Date(model.pricing.updatedAt),
+ changeFrequency: 'monthly',
+ priority: 0.6,
}))
const staticPages: MetadataRoute.Sitemap = [
{
url: baseUrl,
lastModified: now,
+ changeFrequency: 'daily',
+ priority: 1.0,
},
{
url: `${baseUrl}/blog`,
@@ -52,10 +66,6 @@ export default async function sitemap(): Promise {
url: `${baseUrl}/blog/tags`,
lastModified: now,
},
- // {
- // url: `${baseUrl}/templates`,
- // lastModified: now,
- // },
{
url: `${baseUrl}/changelog`,
lastModified: now,
diff --git a/apps/sim/ee/whitelabeling/metadata.ts b/apps/sim/ee/whitelabeling/metadata.ts
index 16dce2da97d..cfaefd63f47 100644
--- a/apps/sim/ee/whitelabeling/metadata.ts
+++ b/apps/sim/ee/whitelabeling/metadata.ts
@@ -9,8 +9,8 @@ export function generateBrandedMetadata(override: Partial = {}): Metad
const brand = getBrandConfig()
const defaultTitle = brand.name
- const summaryFull = `Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders — from startups to Fortune 500 companies. SOC2 compliant.`
- const summaryShort = `Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.`
+ const summaryFull = `Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code. Trusted by over 100,000 builders — from startups to Fortune 500 companies. SOC2 compliant.`
+ const summaryShort = `Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.`
return {
title: {
@@ -22,8 +22,12 @@ export function generateBrandedMetadata(override: Partial = {}): Metad
authors: [{ name: brand.name }],
generator: 'Next.js',
keywords: [
+ 'AI workspace',
+ 'AI agent builder',
+ 'AI agent workflow builder',
+ 'build AI agents',
+ 'visual workflow builder',
'AI agents',
- 'agentic workforce',
'AI agent platform',
'open-source AI agents',
'agentic workflows',
@@ -132,11 +136,11 @@ export function generateStructuredData() {
'@type': 'SoftwareApplication',
name: 'Sim',
description:
- 'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders. SOC2 compliant.',
+ 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work. Trusted by over 100,000 builders. SOC2 compliant.',
url: getBaseUrl(),
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web',
- applicationSubCategory: 'AIAgentPlatform',
+ applicationSubCategory: 'AIWorkspace',
areaServed: 'Worldwide',
availableLanguage: ['en'],
offers: {
@@ -149,8 +153,9 @@ export function generateStructuredData() {
url: 'https://sim.ai',
},
featureList: [
- 'AI Agent Creation',
- 'Agentic Workflow Orchestration',
+ 'AI Workspace for Teams',
+ 'Mothership — Natural Language Agent Creation',
+ 'Visual Workflow Builder',
'1,000+ Integrations',
'LLM Orchestration',
'Knowledge Base Creation',
diff --git a/apps/sim/lib/blog/seo.ts b/apps/sim/lib/blog/seo.ts
index faee69c236b..d7e7693158c 100644
--- a/apps/sim/lib/blog/seo.ts
+++ b/apps/sim/lib/blog/seo.ts
@@ -151,7 +151,7 @@ export function buildCollectionPageJsonLd() {
'@type': 'CollectionPage',
name: 'Sim Blog',
url: 'https://sim.ai/blog',
- description: 'Announcements, insights, and guides for building AI agent workflows.',
+ description: 'Announcements, insights, and guides for building AI agents.',
publisher: {
'@type': 'Organization',
name: 'Sim',
diff --git a/apps/sim/lib/posthog/events.ts b/apps/sim/lib/posthog/events.ts
index faf9895bf62..9e0cdc6637c 100644
--- a/apps/sim/lib/posthog/events.ts
+++ b/apps/sim/lib/posthog/events.ts
@@ -16,8 +16,31 @@ export interface PostHogEventMap {
landing_cta_clicked: {
label: string
- section: 'hero' | 'navbar' | 'footer_cta' | 'pricing'
- destination: string
+ section:
+ | 'hero'
+ | 'navbar'
+ | 'footer_cta'
+ | 'pricing'
+ | 'features'
+ | 'collaboration'
+ | 'templates'
+ | 'landing_preview'
+ | 'integrations'
+ destination: 'auth_modal' | 'demo_modal' | '/signup' | '/login' | '/workspace' | (string & {})
+ }
+
+ auth_modal_opened: {
+ view: 'login' | 'signup'
+ source:
+ | 'hero'
+ | 'navbar'
+ | 'mobile_navbar'
+ | 'footer_cta'
+ | 'pricing'
+ | 'features'
+ | 'collaboration'
+ | 'landing_preview'
+ | 'integrations'
}
landing_demo_request_submitted: {
@@ -26,6 +49,8 @@ export interface PostHogEventMap {
landing_prompt_submitted: Record
+ login_page_viewed: Record
+
signup_page_viewed: Record
signup_failed: {
diff --git a/apps/sim/public/llms.txt b/apps/sim/public/llms.txt
index af8e397fa84..ea33e518c7e 100644
--- a/apps/sim/public/llms.txt
+++ b/apps/sim/public/llms.txt
@@ -1,6 +1,6 @@
# Sim
-Sim is an open-source platform for building, testing, and deploying AI agent workflows visually. Create powerful AI agents, automation pipelines, and data processing workflows by connecting blocks on a canvas.
+Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Create agents visually with the workflow builder, conversationally through Mothership, or programmatically with the API — connected to 1,000+ integrations and every major LLM.
## Key Facts
@@ -8,31 +8,52 @@ Sim is an open-source platform for building, testing, and deploying AI agent wor
- GitHub: https://github.com/simstudioai/sim
- Documentation: https://docs.sim.ai
- License: Apache 2.0
-- Category: AI workflow automation, developer tools
+- Category: AI workspace, AI agent builder, developer tools
+- Trusted by: 100,000+ builders, from startups to Fortune 500
## What Sim Does
-- Visual workflow builder with drag-and-drop interface for AI agents
-- 80+ built-in integrations (OpenAI, Anthropic, Slack, Gmail, GitHub, and more)
-- Real-time team collaboration on workflows
-- Multiple deployment options: cloud-hosted or self-hosted
-- Custom integrations via MCP (Model Context Protocol)
-- API and SDK access (TypeScript, Python)
-- Workflow execution engine with logging and debugging
+Sim is a unified AI workspace with these core modules:
+
+- **Mothership** — AI command center. Build and manage everything in natural language.
+- **Workflows** — Visual builder. Connect blocks, models, and integrations into agent logic.
+- **Knowledge Base** — Upload docs, sync sources, build vector databases for agent memory.
+- **Tables** — Built-in database. Store, query, and wire structured data into agent runs.
+- **Files** — Upload, create, and share documents across your team and agents.
+- **Logs** — Full execution tracing. Inputs, outputs, cost, and duration for every run.
+
+## Capabilities
+
+- 1,000+ integrations (Slack, Gmail, GitHub, Notion, Jira, Salesforce, HubSpot, and more)
+- Every major LLM: OpenAI, Anthropic, Google Gemini, xAI Grok, Mistral, Groq, Cerebras
+- Real-time team collaboration with multiplayer editing
+- Enterprise governance: RBAC, staging/production environments, deployment versioning, audit logs
+- Self-hosting via Docker, bring-your-own-key (BYOK) for all model providers
+- API, CLI, and SDK access (TypeScript, Python)
+- MCP (Model Context Protocol) server creation and connection
+- SOC2 compliant
+
+## Key Pages
+
+- AI Models Directory: https://sim.ai/models
+- Integrations: https://sim.ai/integrations
+- Pricing: https://sim.ai/#pricing
+- Partners: https://sim.ai/partners
## Blog
-The Sim blog covers announcements, technical deep-dives, and guides for building AI agent workflows.
+The Sim blog covers announcements, technical deep-dives, and guides for building AI agents.
- Blog: https://sim.ai/blog
- RSS: https://sim.ai/blog/rss.xml
-## Documentation Sections
+## Documentation
- Getting Started: https://docs.sim.ai/getting-started
- Blocks: https://docs.sim.ai/blocks
- Tools & Integrations: https://docs.sim.ai/tools
- Webhooks: https://docs.sim.ai/webhooks
- MCP Protocol: https://docs.sim.ai/mcp
-- Deployment: https://docs.sim.ai/deployment
+- Self-Hosting: https://docs.sim.ai/self-hosting
+- API Reference: https://docs.sim.ai/api-reference/getting-started
- SDKs: https://docs.sim.ai/sdks
From 85f1d968591d3d4dd037dfd48b8e64f94edf659f Mon Sep 17 00:00:00 2001
From: Waleed
Date: Sat, 11 Apr 2026 20:41:37 -0700
Subject: [PATCH 7/7] feat(ee): enterprise feature flags, permission group
platform controls, audit logs ui, delete account (#4115)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(ee): enterprise feature flags, permission group platform controls, audit logs ui, delete account
* fix(settings): improve sidebar skeleton fidelity and fix credit purchase org cache invalidation
- Bump skeleton icon and text from 16/14px to 24px to better match real nav item visual weight
- Add orgId support to usePurchaseCredits so org billing/subscription caches are invalidated on credit purchase, matching the pattern used by useUpgradeSubscription
- Polish ColorInput in whitelabeling settings with auto-prefix and select-on-focus UX
* revert(settings): remove delete account feature
* fix(settings): address pr review — atomic autoAddNewMembers, extract query hook, fix types and signal forwarding
* chore(helm): add CREDENTIAL_SETS_ENABLED to values.yaml
* fix(access-control): dynamic platform category columns, atomic permission group delete
* fix(access-control): restore triggers section in blocks tab
* fix(access-control): merge triggers into tools section in blocks tab
* upgrade tubro
* fix(access-control): fix Select All state when config has stale blacklisted provider IDs
* fix(access-control): derive platform Select All from features list; revert turbo schema version
* fix(access-control): fix blocks Select All check, filter empty platform columns
* revert(settings): restore original skeleton icon and text sizes
---
.../docs/content/docs/en/enterprise/index.mdx | 3 +
.../app/api/permission-groups/[id]/route.ts | 59 +-
apps/sim/app/api/permission-groups/route.ts | 32 +-
.../api/settings/allowed-providers/route.ts | 14 +
.../[workspaceId]/settings/[section]/page.tsx | 5 +-
.../settings/[section]/prefetch.ts | 23 +
.../settings/[section]/settings.tsx | 3 +-
.../[workspaceId]/settings/layout.tsx | 2 +-
.../[workspaceId]/settings/navigation.ts | 5 +-
.../settings-sidebar/settings-sidebar.tsx | 218 +++----
.../emcn/components/modal/modal.tsx | 4 +-
apps/sim/components/icons.tsx | 2 +-
.../components/access-control.tsx | 548 ++++++++++--------
.../utils/permission-check.test.ts | 4 +
.../ee/audit-logs/components/audit-logs.tsx | 292 +++++++---
apps/sim/ee/sso/hooks/sso.ts | 15 +-
.../components/whitelabeling-settings.tsx | 40 +-
apps/sim/hooks/queries/allowed-providers.ts | 35 ++
apps/sim/hooks/queries/subscription.ts | 7 +-
apps/sim/lib/core/config/env.ts | 8 +
apps/sim/lib/core/config/feature-flags.ts | 23 +
apps/sim/lib/permission-groups/types.ts | 14 +
apps/sim/providers/utils.ts | 10 +-
bun.lock | 16 +-
helm/sim/values.yaml | 10 +
package.json | 2 +-
26 files changed, 873 insertions(+), 521 deletions(-)
create mode 100644 apps/sim/app/api/settings/allowed-providers/route.ts
create mode 100644 apps/sim/hooks/queries/allowed-providers.ts
diff --git a/apps/docs/content/docs/en/enterprise/index.mdx b/apps/docs/content/docs/en/enterprise/index.mdx
index 69fc92e8a12..0cd2aa9dbae 100644
--- a/apps/docs/content/docs/en/enterprise/index.mdx
+++ b/apps/docs/content/docs/en/enterprise/index.mdx
@@ -69,6 +69,9 @@ For self-hosted deployments, enterprise features can be enabled via environment
| `ACCESS_CONTROL_ENABLED`, `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` | Permission groups for access restrictions |
| `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Single Sign-On with SAML/OIDC |
| `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Polling Groups for email triggers |
+| `INBOX_ENABLED`, `NEXT_PUBLIC_INBOX_ENABLED` | Sim Mailer inbox for outbound email |
+| `WHITELABELING_ENABLED`, `NEXT_PUBLIC_WHITELABELING_ENABLED` | Custom branding and white-labeling |
+| `AUDIT_LOGS_ENABLED`, `NEXT_PUBLIC_AUDIT_LOGS_ENABLED` | Audit logging for compliance and monitoring |
| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Globally disable workspace/organization invitations |
### Organization Management
diff --git a/apps/sim/app/api/permission-groups/[id]/route.ts b/apps/sim/app/api/permission-groups/[id]/route.ts
index 51cbe1222b6..7cab684f043 100644
--- a/apps/sim/app/api/permission-groups/[id]/route.ts
+++ b/apps/sim/app/api/permission-groups/[id]/route.ts
@@ -21,7 +21,10 @@ const configSchema = z.object({
hideKnowledgeBaseTab: z.boolean().optional(),
hideTablesTab: z.boolean().optional(),
hideCopilot: z.boolean().optional(),
+ hideIntegrationsTab: z.boolean().optional(),
+ hideSecretsTab: z.boolean().optional(),
hideApiKeysTab: z.boolean().optional(),
+ hideInboxTab: z.boolean().optional(),
hideEnvironmentTab: z.boolean().optional(),
hideFilesTab: z.boolean().optional(),
disableMcpTools: z.boolean().optional(),
@@ -29,6 +32,7 @@ const configSchema = z.object({
disableSkills: z.boolean().optional(),
hideTemplates: z.boolean().optional(),
disableInvitations: z.boolean().optional(),
+ disablePublicApi: z.boolean().optional(),
hideDeployApi: z.boolean().optional(),
hideDeployMcp: z.boolean().optional(),
hideDeployA2a: z.boolean().optional(),
@@ -151,31 +155,34 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
? { ...currentConfig, ...updates.config }
: currentConfig
- // If setting autoAddNewMembers to true, unset it on other groups in the org first
- if (updates.autoAddNewMembers === true) {
- await db
- .update(permissionGroup)
- .set({ autoAddNewMembers: false, updatedAt: new Date() })
- .where(
- and(
- eq(permissionGroup.organizationId, result.group.organizationId),
- eq(permissionGroup.autoAddNewMembers, true)
+ const now = new Date()
+
+ await db.transaction(async (tx) => {
+ if (updates.autoAddNewMembers === true) {
+ await tx
+ .update(permissionGroup)
+ .set({ autoAddNewMembers: false, updatedAt: now })
+ .where(
+ and(
+ eq(permissionGroup.organizationId, result.group.organizationId),
+ eq(permissionGroup.autoAddNewMembers, true)
+ )
)
- )
- }
+ }
- await db
- .update(permissionGroup)
- .set({
- ...(updates.name !== undefined && { name: updates.name }),
- ...(updates.description !== undefined && { description: updates.description }),
- ...(updates.autoAddNewMembers !== undefined && {
- autoAddNewMembers: updates.autoAddNewMembers,
- }),
- config: newConfig,
- updatedAt: new Date(),
- })
- .where(eq(permissionGroup.id, id))
+ await tx
+ .update(permissionGroup)
+ .set({
+ ...(updates.name !== undefined && { name: updates.name }),
+ ...(updates.description !== undefined && { description: updates.description }),
+ ...(updates.autoAddNewMembers !== undefined && {
+ autoAddNewMembers: updates.autoAddNewMembers,
+ }),
+ config: newConfig,
+ updatedAt: now,
+ })
+ .where(eq(permissionGroup.id, id))
+ })
const [updated] = await db
.select()
@@ -245,8 +252,10 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 })
}
- await db.delete(permissionGroupMember).where(eq(permissionGroupMember.permissionGroupId, id))
- await db.delete(permissionGroup).where(eq(permissionGroup.id, id))
+ await db.transaction(async (tx) => {
+ await tx.delete(permissionGroupMember).where(eq(permissionGroupMember.permissionGroupId, id))
+ await tx.delete(permissionGroup).where(eq(permissionGroup.id, id))
+ })
logger.info('Deleted permission group', { permissionGroupId: id, userId: session.user.id })
diff --git a/apps/sim/app/api/permission-groups/route.ts b/apps/sim/app/api/permission-groups/route.ts
index 9b88d482617..dd5c09e5453 100644
--- a/apps/sim/app/api/permission-groups/route.ts
+++ b/apps/sim/app/api/permission-groups/route.ts
@@ -23,7 +23,10 @@ const configSchema = z.object({
hideKnowledgeBaseTab: z.boolean().optional(),
hideTablesTab: z.boolean().optional(),
hideCopilot: z.boolean().optional(),
+ hideIntegrationsTab: z.boolean().optional(),
+ hideSecretsTab: z.boolean().optional(),
hideApiKeysTab: z.boolean().optional(),
+ hideInboxTab: z.boolean().optional(),
hideEnvironmentTab: z.boolean().optional(),
hideFilesTab: z.boolean().optional(),
disableMcpTools: z.boolean().optional(),
@@ -31,6 +34,7 @@ const configSchema = z.object({
disableSkills: z.boolean().optional(),
hideTemplates: z.boolean().optional(),
disableInvitations: z.boolean().optional(),
+ disablePublicApi: z.boolean().optional(),
hideDeployApi: z.boolean().optional(),
hideDeployMcp: z.boolean().optional(),
hideDeployA2a: z.boolean().optional(),
@@ -167,19 +171,6 @@ export async function POST(req: Request) {
...config,
}
- // If autoAddNewMembers is true, unset it on any existing groups first
- if (autoAddNewMembers) {
- await db
- .update(permissionGroup)
- .set({ autoAddNewMembers: false, updatedAt: new Date() })
- .where(
- and(
- eq(permissionGroup.organizationId, organizationId),
- eq(permissionGroup.autoAddNewMembers, true)
- )
- )
- }
-
const now = new Date()
const newGroup = {
id: generateId(),
@@ -193,7 +184,20 @@ export async function POST(req: Request) {
autoAddNewMembers: autoAddNewMembers || false,
}
- await db.insert(permissionGroup).values(newGroup)
+ await db.transaction(async (tx) => {
+ if (autoAddNewMembers) {
+ await tx
+ .update(permissionGroup)
+ .set({ autoAddNewMembers: false, updatedAt: now })
+ .where(
+ and(
+ eq(permissionGroup.organizationId, organizationId),
+ eq(permissionGroup.autoAddNewMembers, true)
+ )
+ )
+ }
+ await tx.insert(permissionGroup).values(newGroup)
+ })
logger.info('Created permission group', {
permissionGroupId: newGroup.id,
diff --git a/apps/sim/app/api/settings/allowed-providers/route.ts b/apps/sim/app/api/settings/allowed-providers/route.ts
new file mode 100644
index 00000000000..2880c9eca08
--- /dev/null
+++ b/apps/sim/app/api/settings/allowed-providers/route.ts
@@ -0,0 +1,14 @@
+import { NextResponse } from 'next/server'
+import { getSession } from '@/lib/auth'
+import { getBlacklistedProvidersFromEnv } from '@/lib/core/config/feature-flags'
+
+export async function GET() {
+ const session = await getSession()
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ return NextResponse.json({
+ blacklistedProviders: getBlacklistedProvidersFromEnv(),
+ })
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx
index ca48abef01b..2c0db1d6e1d 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx
@@ -1,8 +1,9 @@
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import type { Metadata } from 'next'
+import { isBillingEnabled } from '@/lib/core/config/feature-flags'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation'
-import { prefetchGeneralSettings, prefetchUserProfile } from './prefetch'
+import { prefetchGeneralSettings, prefetchSubscriptionData, prefetchUserProfile } from './prefetch'
import { SettingsPage } from './settings'
const SECTION_TITLES: Record = {
@@ -11,6 +12,7 @@ const SECTION_TITLES: Record = {
secrets: 'Secrets',
'template-profile': 'Template Profile',
'access-control': 'Access Control',
+ 'audit-logs': 'Audit Logs',
apikeys: 'Sim Keys',
byok: 'BYOK',
subscription: 'Subscription',
@@ -46,6 +48,7 @@ export default async function SettingsSectionPage({
void prefetchGeneralSettings(queryClient)
void prefetchUserProfile(queryClient)
+ if (isBillingEnabled) void prefetchSubscriptionData(queryClient)
return (
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts b/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts
index 7ac50861e5b..d04d9481d1a 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts
+++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/prefetch.ts
@@ -2,6 +2,7 @@ import type { QueryClient } from '@tanstack/react-query'
import { headers } from 'next/headers'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { generalSettingsKeys, mapGeneralSettingsResponse } from '@/hooks/queries/general-settings'
+import { subscriptionKeys } from '@/hooks/queries/subscription'
import { mapUserProfileResponse, userProfileKeys } from '@/hooks/queries/user-profile'
/**
@@ -35,6 +36,28 @@ export function prefetchGeneralSettings(queryClient: QueryClient) {
})
}
+/**
+ * Prefetch subscription data server-side via internal API fetch.
+ * Uses the same query key as the client `useSubscriptionData` hook (with includeOrg=false)
+ * so data is shared via HydrationBoundary — ensuring the settings sidebar renders
+ * with the correct Team/Enterprise tabs on the first paint, with no flash.
+ */
+export function prefetchSubscriptionData(queryClient: QueryClient) {
+ return queryClient.prefetchQuery({
+ queryKey: subscriptionKeys.user(false),
+ queryFn: async () => {
+ const fwdHeaders = await getForwardedHeaders()
+ const baseUrl = getInternalApiBaseUrl()
+ const response = await fetch(`${baseUrl}/api/billing?context=user`, {
+ headers: fwdHeaders,
+ })
+ if (!response.ok) throw new Error(`Subscription prefetch failed: ${response.status}`)
+ return response.json()
+ },
+ staleTime: 5 * 60 * 1000,
+ })
+}
+
/**
* Prefetch user profile server-side via internal API fetch.
* Uses the same query keys as the client `useUserProfile` hook
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx
index b6f439635b9..9610babe879 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx
@@ -6,6 +6,7 @@ import { useSearchParams } from 'next/navigation'
import { usePostHog } from 'posthog-js/react'
import { Skeleton } from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
+import { cn } from '@/lib/core/utils/cn'
import { captureEvent } from '@/lib/posthog/client'
import { AdminSkeleton } from '@/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton'
import { ApiKeysSkeleton } from '@/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton'
@@ -198,7 +199,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
}, [effectiveSection, sessionLoading, posthog])
return (
-
+
{label}
{effectiveSection === 'general' &&
}
{effectiveSection === 'integrations' &&
}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx b/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx
index 0fab587de01..06cebe3773b 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/layout.tsx
@@ -1,7 +1,7 @@
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
return (
-
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts
index ff25389fc0c..222bb1962aa 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts
+++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts
@@ -74,6 +74,8 @@ const isSSOEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
const isCredentialSetsEnabled = isTruthy(getEnv('NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED'))
const isAccessControlEnabled = isTruthy(getEnv('NEXT_PUBLIC_ACCESS_CONTROL_ENABLED'))
const isInboxEnabled = isTruthy(getEnv('NEXT_PUBLIC_INBOX_ENABLED'))
+const isWhitelabelingEnabled = isTruthy(getEnv('NEXT_PUBLIC_WHITELABELING_ENABLED'))
+const isAuditLogsEnabled = isTruthy(getEnv('NEXT_PUBLIC_AUDIT_LOGS_ENABLED'))
export const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED'))
export { isCredentialSetsEnabled }
@@ -106,6 +108,7 @@ export const allNavigationItems: NavigationItem[] = [
section: 'enterprise',
requiresHosted: true,
requiresEnterprise: true,
+ selfHostedOverride: isAuditLogsEnabled,
},
{
id: 'subscription',
@@ -181,7 +184,7 @@ export const allNavigationItems: NavigationItem[] = [
section: 'enterprise',
requiresHosted: true,
requiresEnterprise: true,
- selfHostedOverride: isBillingEnabled,
+ selfHostedOverride: isWhitelabelingEnabled,
},
{
id: 'admin',
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx
index ccb7cba760b..9c1bc3fafcb 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx
@@ -34,7 +34,23 @@ import { usePermissionConfig } from '@/hooks/use-permission-config'
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
import { useSettingsDirtyStore } from '@/stores/settings/dirty/store'
-const SKELETON_SECTIONS = [3, 2, 2] as const
+const SKELETON_SECTIONS = sectionConfig
+ .map(({ key }) =>
+ Math.min(
+ allNavigationItems.filter(
+ (item) =>
+ item.section === key &&
+ !(item.hideWhenBillingDisabled && !isBillingEnabled) &&
+ !item.requiresTeam &&
+ !item.requiresEnterprise &&
+ !item.requiresSuperUser &&
+ !item.requiresAdminRole &&
+ item.id !== 'template-profile'
+ ).length,
+ 3
+ )
+ )
+ .filter((count) => count > 0)
interface SettingsSidebarProps {
isCollapsed?: boolean
@@ -61,14 +77,16 @@ export function SettingsSidebar({
const { data: session, isPending: sessionLoading } = useSession()
const { data: organizationsData, isLoading: orgsLoading } = useOrganizations()
const { data: generalSettings } = useGeneralSettings()
- const { data: subscriptionData } = useSubscriptionData({
+ const { data: subscriptionData, isLoading: subscriptionLoading } = useSubscriptionData({
enabled: isBillingEnabled,
staleTime: 5 * 60 * 1000,
})
- const { data: ssoProvidersData, isLoading: isLoadingSSO } = useSSOProviders()
+ const { data: ssoProvidersData, isLoading: isLoadingSSO } = useSSOProviders({
+ enabled: !isHosted,
+ })
const activeOrganization = organizationsData?.activeOrganization
- const { config: permissionConfig } = usePermissionConfig()
+ const { config: permissionConfig, isLoading: permissionLoading } = usePermissionConfig()
const userEmail = session?.user?.email
const userId = session?.user?.id
@@ -100,9 +118,18 @@ export function SettingsSidebar({
if (item.id === 'template-profile') {
return false
}
+ if (item.id === 'integrations' && permissionConfig.hideIntegrationsTab) {
+ return false
+ }
+ if (item.id === 'secrets' && permissionConfig.hideSecretsTab) {
+ return false
+ }
if (item.id === 'apikeys' && permissionConfig.hideApiKeysTab) {
return false
}
+ if (item.id === 'inbox' && permissionConfig.hideInboxTab) {
+ return false
+ }
if (item.id === 'mcp' && permissionConfig.disableMcpTools) {
return false
}
@@ -244,113 +271,102 @@ export function SettingsSidebar({
!isCollapsed && 'overflow-y-auto overflow-x-hidden'
)}
>
- {sessionLoading || orgsLoading ? (
- isCollapsed ? (
- <>
- {SKELETON_SECTIONS.map((count, sectionIdx) => (
-
- {Array.from({ length: count }, (_, i) => (
-
-
-
- ))}
-
- ))}
- >
- ) : (
- Array.from({ length: 3 }, (_, i) => (
-
-
+ {sessionLoading ||
+ orgsLoading ||
+ (isBillingEnabled && subscriptionLoading) ||
+ permissionLoading ||
+ (!isHosted && isLoadingSSO)
+ ? SKELETON_SECTIONS.map((count, i) => (
+
+
- {Array.from({ length: i === 0 ? 3 : 2 }, (_, j) => (
-
-
+ {Array.from({ length: count }, (_, j) => (
+
+
+
))}
))
- )
- ) : (
- sectionConfig.map(({ key, title }) => {
- const sectionItems = navigationItems.filter((item) => item.section === key)
- if (sectionItems.length === 0) return null
-
- return (
-
-
-
- {sectionItems.map((item) => {
- const Icon = item.icon
- const active = activeSection === item.id
- const isLocked = item.requiresMax && !subscriptionAccess.hasUsableMaxAccess
- const itemClassName = cn(
- 'group mx-0.5 flex h-[30px] items-center gap-2 rounded-[8px] px-2 text-[14px]',
- !active && 'hover-hover:bg-[var(--surface-hover)]',
- active && 'bg-[var(--surface-active)]'
- )
- const content = (
- <>
-
-
- {item.label}
-
- {isLocked && (
-
- Max
+ : sectionConfig.map(({ key, title }) => {
+ const sectionItems = navigationItems.filter((item) => item.section === key)
+ if (sectionItems.length === 0) return null
+
+ return (
+
+
+
+ {sectionItems.map((item) => {
+ const Icon = item.icon
+ const active = activeSection === item.id
+ const isLocked = item.requiresMax && !subscriptionAccess.hasUsableMaxAccess
+ const itemClassName = cn(
+ 'group mx-0.5 flex h-[30px] items-center gap-2 rounded-[8px] px-2 text-[14px]',
+ !active && 'hover-hover:bg-[var(--surface-hover)]',
+ active && 'bg-[var(--surface-active)]'
+ )
+ const content = (
+ <>
+
+
+ {item.label}
- )}
- >
- )
-
- const element = item.externalUrl ? (
-
- {content}
-
- ) : (
-
handlePrefetch(item.id)}
- onFocus={() => handlePrefetch(item.id)}
- onClick={() => {
- const section = item.id as SettingsSection
- if (section === activeSection) return
- if (!requestNavigation(section)) {
- setShowDiscardDialog(true)
- return
- }
- router.replace(getSettingsHref({ section }), { scroll: false })
- }}
- >
- {content}
-
- )
-
- return (
-
- {element}
-
- )
- })}
+ {isLocked && (
+
+ Max
+
+ )}
+ >
+ )
+
+ const element = item.externalUrl ? (
+
+ {content}
+
+ ) : (
+
handlePrefetch(item.id)}
+ onFocus={() => handlePrefetch(item.id)}
+ onClick={() => {
+ const section = item.id as SettingsSection
+ if (section === activeSection) return
+ if (!requestNavigation(section)) {
+ setShowDiscardDialog(true)
+ return
+ }
+ router.replace(getSettingsHref({ section }), { scroll: false })
+ }}
+ >
+ {content}
+
+ )
+
+ return (
+
+ {element}
+
+ )
+ })}
+
-
- )
- })
- )}
+ )
+ })}
!open && handleCancelDiscard()}>
diff --git a/apps/sim/components/emcn/components/modal/modal.tsx b/apps/sim/components/emcn/components/modal/modal.tsx
index 3e2341ce0cf..91fc76ebd93 100644
--- a/apps/sim/components/emcn/components/modal/modal.tsx
+++ b/apps/sim/components/emcn/components/modal/modal.tsx
@@ -102,7 +102,7 @@ ModalOverlay.displayName = 'ModalOverlay'
* Each size uses viewport units with sensible min/max constraints.
*/
const MODAL_SIZES = {
- sm: 'w-[90vw] max-w-[400px]',
+ sm: 'w-[90vw] max-w-[440px]',
md: 'w-[90vw] max-w-[500px]',
lg: 'w-[90vw] max-w-[600px]',
xl: 'w-[90vw] max-w-[800px]',
@@ -120,7 +120,7 @@ export interface ModalContentProps
showClose?: boolean
/**
* Modal size variant with responsive viewport-based sizing.
- * - sm: max 400px (dialogs, confirmations)
+ * - sm: max 440px (dialogs, confirmations)
* - md: max 500px (default, forms)
* - lg: max 600px (content-heavy modals)
* - xl: max 800px (complex editors)
diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx
index 2806cd4d314..0e4de0bf5e5 100644
--- a/apps/sim/components/icons.tsx
+++ b/apps/sim/components/icons.tsx
@@ -3554,7 +3554,7 @@ export function FireworksIcon(props: SVGProps) {
>
)
diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx
index cc636fb9df8..65cc642e13d 100644
--- a/apps/sim/ee/access-control/components/access-control.tsx
+++ b/apps/sim/ee/access-control/components/access-control.tsx
@@ -41,6 +41,7 @@ import {
useRemovePermissionGroupMember,
useUpdatePermissionGroup,
} from '@/ee/access-control/hooks/permission-groups'
+import { useBlacklistedProviders } from '@/hooks/queries/allowed-providers'
import { useOrganization, useOrganizations } from '@/hooks/queries/organization'
import { useSubscriptionData } from '@/hooks/queries/subscription'
import { PROVIDER_DEFINITIONS } from '@/providers/models'
@@ -48,10 +49,19 @@ import { getAllProviderIds } from '@/providers/utils'
const logger = createLogger('AccessControl')
+interface OrgMember {
+ userId: string
+ user: {
+ name: string | null
+ email: string
+ image?: string | null
+ }
+}
+
interface AddMembersModalProps {
open: boolean
onOpenChange: (open: boolean) => void
- availableMembers: any[]
+ availableMembers: OrgMember[]
selectedMemberIds: Set
setSelectedMemberIds: React.Dispatch>>
onAddMembers: () => void
@@ -72,7 +82,7 @@ function AddMembersModal({
const filteredMembers = useMemo(() => {
if (!searchTerm.trim()) return availableMembers
const query = searchTerm.toLowerCase()
- return availableMembers.filter((m: any) => {
+ return availableMembers.filter((m) => {
const name = m.user?.name || ''
const email = m.user?.email || ''
return name.toLowerCase().includes(query) || email.toLowerCase().includes(query)
@@ -81,12 +91,12 @@ function AddMembersModal({
const allFilteredSelected = useMemo(() => {
if (filteredMembers.length === 0) return false
- return filteredMembers.every((m: any) => selectedMemberIds.has(m.userId))
+ return filteredMembers.every((m) => selectedMemberIds.has(m.userId))
}, [filteredMembers, selectedMemberIds])
const handleToggleAll = () => {
if (allFilteredSelected) {
- const filteredIds = new Set(filteredMembers.map((m: any) => m.userId))
+ const filteredIds = new Set(filteredMembers.map((m) => m.userId))
setSelectedMemberIds((prev) => {
const next = new Set(prev)
filteredIds.forEach((id) => next.delete(id))
@@ -95,7 +105,7 @@ function AddMembersModal({
} else {
setSelectedMemberIds((prev) => {
const next = new Set(prev)
- filteredMembers.forEach((m: any) => next.add(m.userId))
+ filteredMembers.forEach((m) => next.add(m.userId))
return next
})
}
@@ -140,7 +150,7 @@ function AddMembersModal({
className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
/>
-
+
{allFilteredSelected ? 'Deselect All' : 'Select All'}
@@ -152,7 +162,7 @@ function AddMembersModal({
) : (
- {filteredMembers.map((member: any) => {
+ {filteredMembers.map((member) => {
const name = member.user?.name || 'Unknown'
const email = member.user?.email || ''
const avatarInitial = name.charAt(0).toUpperCase()
@@ -314,16 +324,22 @@ export function AccessControl() {
configKey: 'hideCopilot' as const,
},
{
- id: 'hide-api-keys',
- label: 'API Keys',
+ id: 'hide-integrations',
+ label: 'Integrations',
category: 'Settings Tabs',
- configKey: 'hideApiKeysTab' as const,
+ configKey: 'hideIntegrationsTab' as const,
},
{
- id: 'hide-environment',
- label: 'Environment',
+ id: 'hide-secrets',
+ label: 'Secrets',
category: 'Settings Tabs',
- configKey: 'hideEnvironmentTab' as const,
+ configKey: 'hideSecretsTab' as const,
+ },
+ {
+ id: 'hide-api-keys',
+ label: 'API Keys',
+ category: 'Settings Tabs',
+ configKey: 'hideApiKeysTab' as const,
},
{
id: 'hide-files',
@@ -391,6 +407,12 @@ export function AccessControl() {
category: 'Collaboration',
configKey: 'disableInvitations' as const,
},
+ {
+ id: 'hide-inbox',
+ label: 'Sim Mailer',
+ category: 'Features',
+ configKey: 'hideInboxTab' as const,
+ },
{
id: 'disable-public-api',
label: 'Public API',
@@ -420,6 +442,29 @@ export function AccessControl() {
return categories
}, [filteredPlatformFeatures])
+ const platformCategoryColumns = useMemo(() => {
+ const categoryGroups = [
+ ['Sidebar', 'Deploy Tabs', 'Collaboration'],
+ ['Workflow Panel', 'Tools', 'Features'],
+ ['Settings Tabs', 'Logs'],
+ ]
+
+ const assignedCategories = new Set(categoryGroups.flat())
+ const unassigned = Object.keys(platformCategories).filter((c) => !assignedCategories.has(c))
+ const groups = unassigned.length > 0 ? [...categoryGroups, unassigned] : categoryGroups
+
+ return groups
+ .map((column) =>
+ column
+ .map((category) => ({
+ category,
+ features: platformCategories[category] ?? [],
+ }))
+ .filter((section) => section.features.length > 0)
+ )
+ .filter((column) => column.length > 0)
+ }, [platformCategories])
+
const hasConfigChanges = useMemo(() => {
if (!viewingGroup || !editingConfig) return false
const original = viewingGroup.config
@@ -436,7 +481,14 @@ export function AccessControl() {
return a.name.localeCompare(b.name)
})
}, [])
- const allProviderIds = useMemo(() => getAllProviderIds(), [])
+ const { data: blacklistedProvidersData } = useBlacklistedProviders({ enabled: showConfigModal })
+
+ const allProviderIds = useMemo(() => {
+ const allIds = getAllProviderIds()
+ const blacklist = blacklistedProvidersData?.blacklistedProviders ?? []
+ if (blacklist.length === 0) return allIds
+ return allIds.filter((id) => !blacklist.includes(id.toLowerCase()))
+ }, [blacklistedProvidersData])
const filteredProviders = useMemo(() => {
if (!providerSearchTerm.trim()) return allProviderIds
@@ -450,6 +502,16 @@ export function AccessControl() {
return allBlocks.filter((b) => b.name.toLowerCase().includes(query))
}, [allBlocks, integrationSearchTerm])
+ const filteredCoreBlocks = useMemo(() => {
+ return filteredBlocks.filter((block) => block.category === 'blocks')
+ }, [filteredBlocks])
+
+ const filteredToolBlocks = useMemo(() => {
+ return filteredBlocks
+ .filter((block) => block.category === 'tools' || block.category === 'triggers')
+ .sort((a, b) => a.name.localeCompare(b.name))
+ }, [filteredBlocks])
+
const orgMembers = useMemo(() => {
return organization?.members || []
}, [organization])
@@ -677,7 +739,7 @@ export function AccessControl() {
const availableMembersToAdd = useMemo(() => {
const existingMemberUserIds = new Set(members.map((m) => m.userId))
- return orgMembers.filter((m: any) => !existingMemberUserIds.has(m.userId))
+ return orgMembers.filter((m) => !existingMemberUserIds.has(m.userId))
}, [orgMembers, members])
if (isLoading) {
@@ -841,249 +903,259 @@ export function AccessControl() {
}
}}
>
-
+
Configure Permissions
-
+
Model Providers
Blocks
Platform
-
-
-
-
-
-
- setProviderSearchTerm(e.target.value)}
- className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
- />
-
-
{
- const allAllowed =
- editingConfig?.allowedModelProviders === null ||
- editingConfig?.allowedModelProviders?.length === allProviderIds.length
- setEditingConfig((prev) =>
- prev ? { ...prev, allowedModelProviders: allAllowed ? [] : null } : prev
- )
- }}
- >
- {editingConfig?.allowedModelProviders === null ||
- editingConfig?.allowedModelProviders?.length === allProviderIds.length
- ? 'Deselect All'
- : 'Select All'}
-
+
+
+
+
+
+ setProviderSearchTerm(e.target.value)}
+ className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
+ />
-
- {filteredProviders.map((providerId) => {
- const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon
- const providerName =
- PROVIDER_DEFINITIONS[providerId]?.name ||
- providerId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
- return (
-
-
toggleProvider(providerId)}
- />
-
- {providerName}
-
+
{
+ const allAllowed =
+ editingConfig?.allowedModelProviders === null ||
+ allProviderIds.every((id) =>
+ editingConfig?.allowedModelProviders?.includes(id)
+ )
+ setEditingConfig((prev) =>
+ prev ? { ...prev, allowedModelProviders: allAllowed ? [] : null } : prev
)
- })}
-
+ }}
+ >
+ {editingConfig?.allowedModelProviders === null ||
+ allProviderIds.every((id) =>
+ editingConfig?.allowedModelProviders?.includes(id)
+ )
+ ? 'Deselect All'
+ : 'Select All'}
+
-
-
-
-
-
-
-
-
-
- setIntegrationSearchTerm(e.target.value)}
- className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
- />
-
-
{
- const allAllowed =
- editingConfig?.allowedIntegrations === null ||
- editingConfig?.allowedIntegrations?.length === allBlocks.length
- setEditingConfig((prev) =>
- prev
- ? {
- ...prev,
- allowedIntegrations: allAllowed ? ['start_trigger'] : null,
- }
- : prev
- )
- }}
- >
- {editingConfig?.allowedIntegrations === null ||
- editingConfig?.allowedIntegrations?.length === allBlocks.length
- ? 'Deselect All'
- : 'Select All'}
-
-
-
- {filteredBlocks.map((block) => {
- const BlockIcon = block.icon
- return (
-
-
toggleIntegration(block.type)}
- />
-
- {BlockIcon && (
-
- )}
-
- {block.name}
+
+ {filteredProviders.map((providerId) => {
+ const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon
+ const providerName =
+ PROVIDER_DEFINITIONS[providerId]?.name ||
+ providerId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
+ const checkboxId = `provider-${providerId}`
+ return (
+
+ toggleProvider(providerId)}
+ />
+
- )
- })}
+ {providerName}
+
+ )
+ })}
+
+
+
+
+
+
+
+ setIntegrationSearchTerm(e.target.value)}
+ className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
+ />
+
{
+ const allAllowed =
+ editingConfig?.allowedIntegrations === null ||
+ allBlocks.every((b) =>
+ editingConfig?.allowedIntegrations?.includes(b.type)
+ )
+ setEditingConfig((prev) =>
+ prev
+ ? {
+ ...prev,
+ allowedIntegrations: allAllowed ? ['start_trigger'] : null,
+ }
+ : prev
+ )
+ }}
+ >
+ {editingConfig?.allowedIntegrations === null ||
+ allBlocks.every((b) => editingConfig?.allowedIntegrations?.includes(b.type))
+ ? 'Deselect All'
+ : 'Select All'}
+
-
-
-
-
-
-
-
-
-
-
setPlatformSearchTerm(e.target.value)}
- className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
- />
+
+ {filteredCoreBlocks.length > 0 && (
+
+
+ Core Blocks
+
+
+ {filteredCoreBlocks.map((block) => {
+ const BlockIcon = block.icon
+ const checkboxId = `block-${block.type}`
+ return (
+
+ toggleIntegration(block.type)}
+ />
+
+ {BlockIcon && (
+
+ )}
+
+ {block.name}
+
+ )
+ })}
+
-
{
- const allVisible =
- !editingConfig?.hideKnowledgeBaseTab &&
- !editingConfig?.hideTablesTab &&
- !editingConfig?.hideTemplates &&
- !editingConfig?.hideCopilot &&
- !editingConfig?.hideApiKeysTab &&
- !editingConfig?.hideEnvironmentTab &&
- !editingConfig?.hideFilesTab &&
- !editingConfig?.disableMcpTools &&
- !editingConfig?.disableCustomTools &&
- !editingConfig?.disableSkills &&
- !editingConfig?.hideTraceSpans &&
- !editingConfig?.disableInvitations &&
- !editingConfig?.disablePublicApi &&
- !editingConfig?.hideDeployApi &&
- !editingConfig?.hideDeployMcp &&
- !editingConfig?.hideDeployA2a &&
- !editingConfig?.hideDeployChatbot &&
- !editingConfig?.hideDeployTemplate
- setEditingConfig((prev) =>
- prev
- ? {
- ...prev,
- hideKnowledgeBaseTab: allVisible,
- hideTablesTab: allVisible,
- hideTemplates: allVisible,
- hideCopilot: allVisible,
- hideApiKeysTab: allVisible,
- hideEnvironmentTab: allVisible,
- hideFilesTab: allVisible,
- disableMcpTools: allVisible,
- disableCustomTools: allVisible,
- disableSkills: allVisible,
- hideTraceSpans: allVisible,
- disableInvitations: allVisible,
- disablePublicApi: allVisible,
- hideDeployApi: allVisible,
- hideDeployMcp: allVisible,
- hideDeployA2a: allVisible,
- hideDeployChatbot: allVisible,
- hideDeployTemplate: allVisible,
- }
- : prev
- )
- }}
- >
- {!editingConfig?.hideKnowledgeBaseTab &&
- !editingConfig?.hideTablesTab &&
- !editingConfig?.hideTemplates &&
- !editingConfig?.hideCopilot &&
- !editingConfig?.hideApiKeysTab &&
- !editingConfig?.hideEnvironmentTab &&
- !editingConfig?.hideFilesTab &&
- !editingConfig?.disableMcpTools &&
- !editingConfig?.disableCustomTools &&
- !editingConfig?.disableSkills &&
- !editingConfig?.hideTraceSpans &&
- !editingConfig?.disableInvitations &&
- !editingConfig?.disablePublicApi &&
- !editingConfig?.hideDeployApi &&
- !editingConfig?.hideDeployMcp &&
- !editingConfig?.hideDeployA2a &&
- !editingConfig?.hideDeployChatbot &&
- !editingConfig?.hideDeployTemplate
- ? 'Deselect All'
- : 'Select All'}
-
-
-
- {Object.entries(platformCategories).map(([category, features]) => (
-
-
- {category}
-
-
- {features.map((feature) => (
-
+ )}
+ {filteredToolBlocks.length > 0 && (
+
+
+ Tools
+
+
+ {filteredToolBlocks.map((block) => {
+ const BlockIcon = block.icon
+ const checkboxId = `block-${block.type}`
+ return (
+
- setEditingConfig((prev) =>
- prev
- ? { ...prev, [feature.configKey]: checked !== true }
- : prev
- )
- }
+ id={checkboxId}
+ checked={isIntegrationAllowed(block.type)}
+ onCheckedChange={() => toggleIntegration(block.type)}
/>
-
- {feature.label}
-
-
- ))}
-
+ {BlockIcon && (
+
+ )}
+
+
{block.name}
+
+ )
+ })}
- ))}
+
+ )}
+
+
+
+
+
+
+
+ setPlatformSearchTerm(e.target.value)}
+ className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
+ />
+
{
+ const allVisible = platformFeatures.every(
+ (f) => !editingConfig?.[f.configKey]
+ )
+ setEditingConfig((prev) =>
+ prev
+ ? {
+ ...prev,
+ ...Object.fromEntries(
+ platformFeatures.map((f) => [f.configKey, allVisible])
+ ),
+ }
+ : prev
+ )
+ }}
+ >
+ {platformFeatures.every((f) => !editingConfig?.[f.configKey])
+ ? 'Deselect All'
+ : 'Select All'}
+
+
+
+ {platformCategoryColumns.map((column, columnIndex) => (
+
+ {column.map(({ category, features }) => (
+
+
+ {category}
+
+
+ {features.map((feature) => (
+
+
+ setEditingConfig((prev) =>
+ prev
+ ? { ...prev, [feature.configKey]: checked !== true }
+ : prev
+ )
+ }
+ />
+ {feature.label}
+
+ ))}
+
+
+ ))}
+
+ ))}
-
-
+
+
{
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
+}
+
+function formatMetadataLabel(key: string): string {
+ return key
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
+ .replace(/[_-]+/g, ' ')
+ .replace(/\b\w/g, (char) => char.toUpperCase())
+}
+
+function formatPrimitiveValue(value: string | number | boolean | null): string {
+ if (value === null) return '-'
+ if (typeof value === 'boolean') return value ? 'Yes' : 'No'
+ if (typeof value === 'number') return value.toLocaleString()
+ return value
+}
+
+function renderMetadataValue(value: unknown) {
+ if (value == null) return -
+
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
+ return {formatPrimitiveValue(value)}
+ }
+
+ if (Array.isArray(value)) {
+ if (value.length === 0) {
+ return None
+ }
+
+ const hasComplexValues = value.some((item) => typeof item === 'object' && item !== null)
+ if (!hasComplexValues) {
+ return (
+
+ {value
+ .map((item) => formatPrimitiveValue((item as string | number | boolean | null) ?? null))
+ .join(', ')}
+
+ )
+ }
+
+ return (
+
+ {JSON.stringify(value, null, 2)}
+
+ )
+ }
+
+ if (isRecord(value)) {
+ const entries = Object.entries(value).filter(([, nestedValue]) => nestedValue !== undefined)
+ if (entries.length === 0) {
+ return None
+ }
+
+ const hasComplexValues = entries.some(([, nestedValue]) => {
+ return Array.isArray(nestedValue) || isRecord(nestedValue)
+ })
+
+ if (!hasComplexValues) {
+ return (
+
+ {entries
+ .map(([nestedKey, nestedValue]) => {
+ return `${formatMetadataLabel(nestedKey)}: ${formatPrimitiveValue((nestedValue as string | number | boolean | null) ?? null)}`
+ })
+ .join(' · ')}
+
+ )
+ }
+
+ return (
+
+ {JSON.stringify(value, null, 2)}
+
+ )
+ }
+
+ return (
+
+ {JSON.stringify(value, null, 2)}
+
+ )
+}
+
+function getMetadataEntries(metadata: unknown) {
+ if (!isRecord(metadata)) return []
+
+ return Object.entries(metadata).filter(([key, value]) => {
+ if (value === undefined) return false
+ return !['name', 'description'].includes(key)
+ })
+}
+
interface ActionBadgeProps {
action: string
}
function ActionBadge({ action }: ActionBadgeProps) {
const [, verb] = action.split('.')
- const variant = verb === 'deleted' || verb === 'removed' || verb === 'revoked' ? 'red' : 'default'
+ const variant =
+ verb === 'deleted' || verb === 'removed' || verb === 'revoked' ? 'red' : 'gray-secondary'
return (
-
+
{formatAction(action)}
)
@@ -59,68 +153,86 @@ interface AuditLogRowProps {
function AuditLogRow({ entry }: AuditLogRowProps) {
const [expanded, setExpanded] = useState(false)
const timestamp = formatDateTime(new Date(entry.createdAt))
+ const metadataEntries = getMetadataEntries(entry.metadata)
return (
-
+
setExpanded(!expanded)}
>
-
+
{timestamp}
-
+
{entry.description || entry.resourceName || entry.resourceId || '-'}
-
- {entry.actorEmail || entry.actorName || 'System'}
+
+
+ {entry.actorEmail || entry.actorName || 'System'}
+
+
{expanded && (
-
-
- Resource
-
- {formatResourceType(entry.resourceType)}
- {entry.resourceId && (
- ({entry.resourceId})
- )}
-
-
- {entry.resourceName && (
+
+
- Name
- {entry.resourceName}
+ Resource
+
+ {formatResourceType(entry.resourceType)}
+ {entry.resourceId && (
+ ({entry.resourceId})
+ )}
+
- )}
-
- Actor
-
- {entry.actorName || 'Unknown'}
- {entry.actorEmail && (
- ({entry.actorEmail})
- )}
-
-
- {entry.description && (
-
- Description
- {entry.description}
-
- )}
- {entry.metadata != null &&
- Object.keys(entry.metadata as Record
).length > 0 ? (
+ {entry.resourceName && (
+
+ Name
+ {entry.resourceName}
+
+ )}
-
Details
-
- {JSON.stringify(entry.metadata, null, 2)}
-
+
Actor
+
+ {entry.actorName || 'Unknown'}
+ {entry.actorEmail && (
+ ({entry.actorEmail})
+ )}
+
- ) : null}
+ {entry.description && (
+
+
+ Description
+
+ {entry.description}
+
+ )}
+ {metadataEntries.map(([key, value]) => (
+
+
+ {formatMetadataLabel(key)}
+
+
{renderMetadataValue(value)}
+
+ ))}
+
)}
@@ -178,7 +290,7 @@ export function AuditLogs() {
return (
-
+
@@ -205,7 +317,7 @@ export function AuditLogs() {
value={dateRange}
onChange={setDateRange}
placeholder='Date range'
- size='md'
+ size='sm'
/>
@@ -216,51 +328,47 @@ export function AuditLogs() {
-
-
- Timestamp
-
-
- Event
-
-
- Description
-
-
- Actor
-
-
+
+
+ Timestamp
+ Event
+ Description
+ Actor
+
-
- {isLoading ? (
-
- {Array.from({ length: 8 }).map((_, i) => (
-
-
-
-
-
-
- ))}
-
- ) : allEntries.length === 0 ? (
-
- {debouncedSearch ? `No results for "${debouncedSearch}"` : 'No audit logs found'}
-
- ) : (
-
- {allEntries.map((entry) => (
-
- ))}
- {hasNextPage && (
-
-
- {isFetchingNextPage ? 'Loading...' : 'Load more'}
-
-
- )}
-
- )}
+
+ {isLoading ? (
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+ ))}
+
+ ) : allEntries.length === 0 ? (
+
+ {debouncedSearch ? `No results for "${debouncedSearch}"` : 'No audit logs found'}
+
+ ) : (
+
+ {allEntries.map((entry) => (
+
+ ))}
+ {hasNextPage && (
+
+
+ {isFetchingNextPage ? 'Loading...' : 'Load more'}
+
+
+ )}
+
+ )}
+
)
diff --git a/apps/sim/ee/sso/hooks/sso.ts b/apps/sim/ee/sso/hooks/sso.ts
index 2dfa1592ea4..37c20ffb7fb 100644
--- a/apps/sim/ee/sso/hooks/sso.ts
+++ b/apps/sim/ee/sso/hooks/sso.ts
@@ -14,8 +14,8 @@ export const ssoKeys = {
/**
* Fetch SSO providers
*/
-async function fetchSSOProviders() {
- const response = await fetch('/api/auth/sso/providers')
+async function fetchSSOProviders(signal: AbortSignal) {
+ const response = await fetch('/api/auth/sso/providers', { signal })
if (!response.ok) {
throw new Error('Failed to fetch SSO providers')
}
@@ -25,12 +25,17 @@ async function fetchSSOProviders() {
/**
* Hook to fetch SSO providers
*/
-export function useSSOProviders() {
+interface UseSSOProvidersOptions {
+ enabled?: boolean
+}
+
+export function useSSOProviders({ enabled = true }: UseSSOProvidersOptions = {}) {
return useQuery({
queryKey: ssoKeys.providers(),
- queryFn: fetchSSOProviders,
- staleTime: 5 * 60 * 1000, // 5 minutes
+ queryFn: ({ signal }) => fetchSSOProviders(signal),
+ staleTime: 5 * 60 * 1000,
placeholderData: keepPreviousData,
+ enabled,
})
}
diff --git a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx
index a6946772c9a..fa28f3d85b8 100644
--- a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx
+++ b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx
@@ -4,7 +4,7 @@ import { useCallback, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Loader2, X } from 'lucide-react'
import Image from 'next/image'
-import { Button, Input, Label, Switch } from '@/components/emcn'
+import { Button, Input, Label } from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
import { getSubscriptionAccessState } from '@/lib/billing/client/utils'
import { HEX_COLOR_REGEX } from '@/lib/branding'
@@ -79,6 +79,22 @@ interface ColorInputProps {
function ColorInput({ label, value, onChange, placeholder = '#000000' }: ColorInputProps) {
const isValidHex = !value || HEX_COLOR_REGEX.test(value)
+ const handleChange = useCallback(
+ (e: React.ChangeEvent
) => {
+ let v = e.target.value.trim()
+ if (v && !v.startsWith('#')) {
+ v = `#${v}`
+ }
+ v = v.slice(0, 1) + v.slice(1).replace(/[^0-9a-fA-F]/g, '')
+ onChange(v.slice(0, 7))
+ },
+ [onChange]
+ )
+
+ const handleFocus = useCallback((e: React.FocusEvent) => {
+ e.target.select()
+ }, [])
+
return (
{label}
@@ -92,7 +108,8 @@ function ColorInput({ label, value, onChange, placeholder = '#000000' }: ColorIn
onChange(e.target.value)}
+ onChange={handleChange}
+ onFocus={handleFocus}
placeholder={placeholder}
className={cn(
'h-[36px] font-mono text-[13px]',
@@ -154,7 +171,6 @@ export function WhitelabelingSettings() {
const [documentationUrl, setDocumentationUrl] = useState('')
const [termsUrl, setTermsUrl] = useState('')
const [privacyUrl, setPrivacyUrl] = useState('')
- const [hidePoweredBySim, setHidePoweredBySim] = useState(false)
const [logoUrl, setLogoUrl] = useState(null)
const [wordmarkUrl, setWordmarkUrl] = useState(null)
const [formInitialized, setFormInitialized] = useState(false)
@@ -172,7 +188,6 @@ export function WhitelabelingSettings() {
setDocumentationUrl(savedSettings.documentationUrl ?? '')
setTermsUrl(savedSettings.termsUrl ?? '')
setPrivacyUrl(savedSettings.privacyUrl ?? '')
- setHidePoweredBySim(savedSettings.hidePoweredBySim ?? false)
setLogoUrl(savedSettings.logoUrl ?? null)
setWordmarkUrl(savedSettings.wordmarkUrl ?? null)
setFormInitialized(true)
@@ -222,7 +237,6 @@ export function WhitelabelingSettings() {
documentationUrl: documentationUrl || null,
termsUrl: termsUrl || null,
privacyUrl: privacyUrl || null,
- hidePoweredBySim,
}
try {
@@ -246,7 +260,6 @@ export function WhitelabelingSettings() {
documentationUrl,
termsUrl,
privacyUrl,
- hidePoweredBySim,
])
if (isBillingEnabled) {
@@ -496,21 +509,6 @@ export function WhitelabelingSettings() {
-
- Advanced
-
-
-
- Hide "Powered by Sim" branding
-
-
- Removes the Sim logo from deployed chats and forms.
-
-
-
-
-
-
[...allowedProvidersKeys.all, 'blacklisted'] as const,
+}
+
+interface BlacklistedProvidersResponse {
+ blacklistedProviders: string[]
+}
+
+async function fetchBlacklistedProviders(
+ signal: AbortSignal
+): Promise {
+ const res = await fetch('/api/settings/allowed-providers', { signal })
+ if (!res.ok) return { blacklistedProviders: [] }
+ return res.json()
+}
+
+/**
+ * Hook to fetch the list of blacklisted provider IDs from the server.
+ */
+export function useBlacklistedProviders({ enabled = true }: { enabled?: boolean } = {}) {
+ return useQuery({
+ queryKey: allowedProvidersKeys.blacklisted(),
+ queryFn: ({ signal }) => fetchBlacklistedProviders(signal),
+ staleTime: 5 * 60 * 1000,
+ enabled,
+ })
+}
diff --git a/apps/sim/hooks/queries/subscription.ts b/apps/sim/hooks/queries/subscription.ts
index c30fbff6c3b..3900bb9d383 100644
--- a/apps/sim/hooks/queries/subscription.ts
+++ b/apps/sim/hooks/queries/subscription.ts
@@ -303,6 +303,7 @@ export function useUpgradeSubscription() {
interface PurchaseCreditsParams {
amount: number
requestId: string
+ orgId?: string
}
export function usePurchaseCredits() {
@@ -324,9 +325,13 @@ export function usePurchaseCredits() {
return data
},
- onSuccess: () => {
+ onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: subscriptionKeys.users() })
queryClient.invalidateQueries({ queryKey: subscriptionKeys.usage() })
+ if (variables.orgId) {
+ queryClient.invalidateQueries({ queryKey: organizationKeys.billing(variables.orgId) })
+ queryClient.invalidateQueries({ queryKey: organizationKeys.subscription(variables.orgId) })
+ }
},
})
}
diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts
index 7b329a2fd12..7a0a510bf29 100644
--- a/apps/sim/lib/core/config/env.ts
+++ b/apps/sim/lib/core/config/env.ts
@@ -335,6 +335,10 @@ export const env = createEnv({
// Access Control (Permission Groups) - for self-hosted deployments
ACCESS_CONTROL_ENABLED: z.boolean().optional(), // Enable access control on self-hosted (bypasses plan requirements)
+ // Enterprise Feature Overrides - for self-hosted deployments
+ WHITELABELING_ENABLED: z.boolean().optional(), // Enable whitelabeling on self-hosted (bypasses hosted requirements)
+ AUDIT_LOGS_ENABLED: z.boolean().optional(), // Enable audit logs on self-hosted (bypasses hosted requirements)
+
// Organizations - for self-hosted deployments
ORGANIZATIONS_ENABLED: z.boolean().optional(), // Enable organizations on self-hosted (bypasses plan requirements)
@@ -426,6 +430,8 @@ export const env = createEnv({
NEXT_PUBLIC_SSO_ENABLED: z.boolean().optional(), // Enable SSO login UI components
NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED: z.boolean().optional(), // Enable credential sets (email polling) on self-hosted
NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: z.boolean().optional(), // Enable access control (permission groups) on self-hosted
+ NEXT_PUBLIC_WHITELABELING_ENABLED: z.boolean().optional(), // Enable whitelabeling on self-hosted (bypasses hosted requirements)
+ NEXT_PUBLIC_AUDIT_LOGS_ENABLED: z.boolean().optional(), // Enable audit logs on self-hosted (bypasses hosted requirements)
NEXT_PUBLIC_ORGANIZATIONS_ENABLED: z.boolean().optional(), // Enable organizations on self-hosted (bypasses plan requirements)
NEXT_PUBLIC_DISABLE_INVITATIONS: z.boolean().optional(), // Disable workspace invitations globally (for self-hosted deployments)
NEXT_PUBLIC_DISABLE_PUBLIC_API: z.boolean().optional(), // Disable public API access UI toggle globally
@@ -460,6 +466,8 @@ export const env = createEnv({
NEXT_PUBLIC_SSO_ENABLED: process.env.NEXT_PUBLIC_SSO_ENABLED,
NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED: process.env.NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED,
NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: process.env.NEXT_PUBLIC_ACCESS_CONTROL_ENABLED,
+ NEXT_PUBLIC_WHITELABELING_ENABLED: process.env.NEXT_PUBLIC_WHITELABELING_ENABLED,
+ NEXT_PUBLIC_AUDIT_LOGS_ENABLED: process.env.NEXT_PUBLIC_AUDIT_LOGS_ENABLED,
NEXT_PUBLIC_ORGANIZATIONS_ENABLED: process.env.NEXT_PUBLIC_ORGANIZATIONS_ENABLED,
NEXT_PUBLIC_DISABLE_INVITATIONS: process.env.NEXT_PUBLIC_DISABLE_INVITATIONS,
NEXT_PUBLIC_DISABLE_PUBLIC_API: process.env.NEXT_PUBLIC_DISABLE_PUBLIC_API,
diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts
index b66444d43e7..8e3820c7783 100644
--- a/apps/sim/lib/core/config/feature-flags.ts
+++ b/apps/sim/lib/core/config/feature-flags.ts
@@ -117,6 +117,18 @@ export const isOrganizationsEnabled =
*/
export const isInboxEnabled = isTruthy(env.INBOX_ENABLED)
+/**
+ * Is whitelabeling enabled via env var override
+ * This bypasses hosted requirements for self-hosted deployments
+ */
+export const isWhitelabelingEnabled = isTruthy(env.WHITELABELING_ENABLED)
+
+/**
+ * Is audit logs enabled via env var override
+ * This bypasses hosted requirements for self-hosted deployments
+ */
+export const isAuditLogsEnabled = isTruthy(env.AUDIT_LOGS_ENABLED)
+
/**
* Is E2B enabled for remote code execution
*/
@@ -186,6 +198,17 @@ export function getAllowedIntegrationsFromEnv(): string[] | null {
return parsed.length > 0 ? parsed : null
}
+/**
+ * Returns the list of blacklisted provider IDs from the environment variable.
+ * If not set or empty, returns an empty array (meaning no providers are blacklisted).
+ */
+export function getBlacklistedProvidersFromEnv(): string[] {
+ if (!env.BLACKLISTED_PROVIDERS) return []
+ return env.BLACKLISTED_PROVIDERS.split(',')
+ .map((p) => p.trim().toLowerCase())
+ .filter(Boolean)
+}
+
/**
* Normalizes a domain entry from the ALLOWED_MCP_DOMAINS env var.
* Accepts bare hostnames (e.g., "mcp.company.com") or full URLs (e.g., "https://mcp.company.com").
diff --git a/apps/sim/lib/permission-groups/types.ts b/apps/sim/lib/permission-groups/types.ts
index 52a77d4da50..c76ad7d1a8f 100644
--- a/apps/sim/lib/permission-groups/types.ts
+++ b/apps/sim/lib/permission-groups/types.ts
@@ -6,7 +6,10 @@ export interface PermissionGroupConfig {
hideKnowledgeBaseTab: boolean
hideTablesTab: boolean
hideCopilot: boolean
+ hideIntegrationsTab: boolean
+ hideSecretsTab: boolean
hideApiKeysTab: boolean
+ hideInboxTab: boolean
hideEnvironmentTab: boolean
hideFilesTab: boolean
disableMcpTools: boolean
@@ -30,7 +33,10 @@ export const DEFAULT_PERMISSION_GROUP_CONFIG: PermissionGroupConfig = {
hideKnowledgeBaseTab: false,
hideTablesTab: false,
hideCopilot: false,
+ hideIntegrationsTab: false,
+ hideSecretsTab: false,
hideApiKeysTab: false,
+ hideInboxTab: false,
hideEnvironmentTab: false,
hideFilesTab: false,
disableMcpTools: false,
@@ -61,7 +67,15 @@ export function parsePermissionGroupConfig(config: unknown): PermissionGroupConf
typeof c.hideKnowledgeBaseTab === 'boolean' ? c.hideKnowledgeBaseTab : false,
hideTablesTab: typeof c.hideTablesTab === 'boolean' ? c.hideTablesTab : false,
hideCopilot: typeof c.hideCopilot === 'boolean' ? c.hideCopilot : false,
+ hideIntegrationsTab: typeof c.hideIntegrationsTab === 'boolean' ? c.hideIntegrationsTab : false,
+ hideSecretsTab:
+ typeof c.hideSecretsTab === 'boolean'
+ ? c.hideSecretsTab
+ : typeof c.hideEnvironmentTab === 'boolean'
+ ? c.hideEnvironmentTab
+ : false,
hideApiKeysTab: typeof c.hideApiKeysTab === 'boolean' ? c.hideApiKeysTab : false,
+ hideInboxTab: typeof c.hideInboxTab === 'boolean' ? c.hideInboxTab : false,
hideEnvironmentTab: typeof c.hideEnvironmentTab === 'boolean' ? c.hideEnvironmentTab : false,
hideFilesTab: typeof c.hideFilesTab === 'boolean' ? c.hideFilesTab : false,
disableMcpTools: typeof c.disableMcpTools === 'boolean' ? c.disableMcpTools : false,
diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts
index 40d4a0ebdf7..f4f5d4c9f04 100644
--- a/apps/sim/providers/utils.ts
+++ b/apps/sim/providers/utils.ts
@@ -4,7 +4,7 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { dollarsToCredits } from '@/lib/billing/credits/conversion'
import { env } from '@/lib/core/config/env'
-import { isHosted } from '@/lib/core/config/feature-flags'
+import { getBlacklistedProvidersFromEnv, isHosted } from '@/lib/core/config/feature-flags'
import {
buildCanonicalIndex,
type CanonicalGroup,
@@ -281,14 +281,8 @@ export function getProviderModels(providerId: ProviderId): string[] {
return getProviderModelsFromDefinitions(providerId)
}
-function getBlacklistedProviders(): string[] {
- if (!env.BLACKLISTED_PROVIDERS) return []
- return env.BLACKLISTED_PROVIDERS.split(',').map((p) => p.trim().toLowerCase())
-}
-
export function isProviderBlacklisted(providerId: string): boolean {
- const blacklist = getBlacklistedProviders()
- return blacklist.includes(providerId.toLowerCase())
+ return getBlacklistedProvidersFromEnv().includes(providerId.toLowerCase())
}
/**
diff --git a/bun.lock b/bun.lock
index e05bc532f5e..b071c71851b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -13,7 +13,7 @@
"glob": "13.0.0",
"husky": "9.1.7",
"lint-staged": "16.0.0",
- "turbo": "2.9.3",
+ "turbo": "2.9.5",
},
},
"apps/docs": {
@@ -1498,17 +1498,17 @@
"@trigger.dev/sdk": ["@trigger.dev/sdk@4.4.3", "", { "dependencies": { "@opentelemetry/api": "1.9.0", "@opentelemetry/semantic-conventions": "1.36.0", "@trigger.dev/core": "4.4.3", "chalk": "^5.2.0", "cronstrue": "^2.21.0", "debug": "^4.3.4", "evt": "^2.4.13", "slug": "^6.0.0", "ulid": "^2.3.0", "uncrypto": "^0.1.3", "uuid": "^9.0.0", "ws": "^8.11.0" }, "peerDependencies": { "ai": "^4.2.0 || ^5.0.0 || ^6.0.0", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai"] }, "sha512-ghJkak+PTBJJ9HiHMcnahJmzjsgCzYiIHu5Qj5R7I9q5LS6i7mkx169rB/tOE9HLadd4HSu3yYA5DrH4wXhZuw=="],
- "@turbo/darwin-64": ["@turbo/darwin-64@2.9.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-P8foouaP+y/p+hhEGBoZpzMbpVvUMwPjDpcy6wN7EYfvvyISD1USuV27qWkczecihwuPJzQ1lDBuL8ERcavTyg=="],
+ "@turbo/darwin-64": ["@turbo/darwin-64@2.9.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-qPxhKsLMQP+9+dsmPgAGidi5uNifD4AoAOnEnljab3Qgn0QZRR31Hp+/CgW3Ia5AanWj6JuLLTBYvuQj4mqTWg=="],
- "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-SIzEkvtNdzdI50FJDaIQ6kQGqgSSdFPcdn0wqmmONN6iGKjy6hsT+EH99GP65FsfV7DLZTh2NmtTIRl2kdoz5Q=="],
+ "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vkF/9F/l3aWd4bHxTui5Hh0F5xrTZ4e3rbBsc57zA6O8gNbmHN3B6eZ5psAIP2CnJRZ8ZxRjV3WZHeNXMXkPBw=="],
- "@turbo/linux-64": ["@turbo/linux-64@2.9.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pLRwFmcHHNBvsCySLS6OFabr/07kDT2pxEt/k6eBf/3asiVQZKJ7Rk88AafQx2aYA641qek4RsXvYO3JYpiBug=="],
+ "@turbo/linux-64": ["@turbo/linux-64@2.9.5", "", { "os": "linux", "cpu": "x64" }, "sha512-z/Get5NUaUxm5HSGFqVMICDRjFNsCUhSc4wnFa/PP1QD0NXCjr7bu9a2EM6md/KMCBW0Qe393Ac+UM7/ryDDTw=="],
- "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gy6ApUroC2Nzv+qjGtE/uPNkhHAFU4c8God+zd5Aiv9L9uBgHlxVJpHT3XWl5xwlJZ2KWuMrlHTaS5kmNB+q1Q=="],
+ "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-jyBifaNoI5/NheyswomiZXJvjdAdvT7hDRYzQ4meP0DKGvpXUjnqsD+4/J2YSDQ34OHxFkL30FnSCUIVOh2PHw=="],
- "@turbo/windows-64": ["@turbo/windows-64@2.9.3", "", { "os": "win32", "cpu": "x64" }, "sha512-d0YelTX6hAsB7kIEtGB3PzIzSfAg3yDoUlHwuwJc3adBXUsyUIs0YLG+1NNtuhcDOUGnWQeKUoJ2pGWvbpRj7w=="],
+ "@turbo/windows-64": ["@turbo/windows-64@2.9.5", "", { "os": "win32", "cpu": "x64" }, "sha512-ph24K5uPtvo7UfuyDXnBiB/8XvrO+RQWbbw5zkA/bVNoy9HDiNoIJJj3s62MxT9tjEb6DnPje5PXSz1UR7QAyg=="],
- "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-/08CwpKJl3oRY8nOlh2YgilZVJDHsr60XTNxRhuDeuFXONpUZ5X+Nv65izbG/xBew9qxcJFbDX9/sAmAX+ITcQ=="],
+ "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-6c5RccT/+iR39SdT1G5HyZaD2n57W77o+l0TTfxG/cVlhV94Acyg2gTQW7zUOhW1BeQpBjHzu9x8yVBZwrHh7g=="],
"@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="],
@@ -3644,7 +3644,7 @@
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
- "turbo": ["turbo@2.9.3", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.3", "@turbo/darwin-arm64": "2.9.3", "@turbo/linux-64": "2.9.3", "@turbo/linux-arm64": "2.9.3", "@turbo/windows-64": "2.9.3", "@turbo/windows-arm64": "2.9.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-J/VUvsGRykPb9R8Kh8dHVBOqioDexLk9BhLCU/ZybRR+HN9UR3cURdazFvNgMDt9zPP8TF6K73Z+tplfmi0PqQ=="],
+ "turbo": ["turbo@2.9.5", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.5", "@turbo/darwin-arm64": "2.9.5", "@turbo/linux-64": "2.9.5", "@turbo/linux-arm64": "2.9.5", "@turbo/windows-64": "2.9.5", "@turbo/windows-arm64": "2.9.5" }, "bin": { "turbo": "bin/turbo" } }, "sha512-JXNkRe6H6MjSlk5UQRTjyoKX5YN2zlc2632xcSlSFBao5yvbMWTpv9SNolOZlZmUlcDOHuszPLItbKrvcXnnZA=="],
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml
index 8d9d6906781..7913e730f4a 100644
--- a/helm/sim/values.yaml
+++ b/helm/sim/values.yaml
@@ -237,6 +237,16 @@ app:
SSO_ENABLED: "" # Enable SSO authentication ("true" to enable)
NEXT_PUBLIC_SSO_ENABLED: "" # Show SSO login button in UI ("true" to enable)
+ # Enterprise Feature Overrides (self-hosted)
+ CREDENTIAL_SETS_ENABLED: "" # Enable credential sets (email polling) on self-hosted ("true" to enable)
+ NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED: "" # Show credential sets settings page ("true" to enable)
+ INBOX_ENABLED: "" # Enable Sim Mailer on self-hosted ("true" to enable)
+ NEXT_PUBLIC_INBOX_ENABLED: "" # Show Sim Mailer settings page ("true" to enable)
+ WHITELABELING_ENABLED: "" # Enable whitelabeling on self-hosted ("true" to enable)
+ NEXT_PUBLIC_WHITELABELING_ENABLED: "" # Show whitelabeling settings page ("true" to enable)
+ AUDIT_LOGS_ENABLED: "" # Enable audit logs on self-hosted ("true" to enable)
+ NEXT_PUBLIC_AUDIT_LOGS_ENABLED: "" # Show audit logs settings page ("true" to enable)
+
# AWS Bedrock Credential Mode
# Set to "true" when the deployment uses AWS default credential chain (IAM roles, instance
# profiles, ECS task roles, IRSA, etc.) instead of explicit access key/secret per workflow.
diff --git a/package.json b/package.json
index c7ff404e8c2..69e33c58900 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
"glob": "13.0.0",
"husky": "9.1.7",
"lint-staged": "16.0.0",
- "turbo": "2.9.3"
+ "turbo": "2.9.5"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,scss}": [