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 f4c751b7b7..7b329a2fd1 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 2641b80e11..a6f2505326 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 877035760e..3ec9b30c93 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 e12eb393ba..f599051757 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 ae19f0f295..6e1e6ed975 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 3421cb166c..14468faaf7 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 6b808a19a4..d8aa7bb493 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 9cef227f35..0d1b204e03 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 e830d1acec..991a829695 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 a27f8ac3d2..0c68cb83c0 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 9276f91ad4..9139a80618 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 d79c90dc4e..9554f0aeb4 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 a0beb2fdfd..7c6c07937c 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 9b0c3125be..7ad1ecc5db 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 8506f34378..2312b03d8c 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 9da936d5e8..6314409031 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 5d814efcf3..add60b6342 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 f618deaf86..8fc8fe1af3 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 24f216835e..800ee7e709 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 de544febc9..4570061cc0 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 96ddfc7877..4c6ded37ab 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 cca7ce97b9..37baf53389 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 8c918cfaa8..db3fecc3be 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 aa22275a37..a25fd8abf3 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 db40982e92..6b94430c99 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 da4b2e1f2a..c2e32ad1f5 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 a654c0da43..1e52028a37 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 70b5f9a5c9..bd61c373a3 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 24b2b8205c..ad25e363a0 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 3288cc0c6f..55f578f2cd 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 8105f983f0..93074304c9 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 36ce192e5d..9dc16f6eaa 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 7e8698d5a9..a0056bf1e6 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 a73d61e262..5b1b4c630e 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 dc30c81b20..138599280d 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 772e254b93..89d18c77e6 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 2b39cf1ab8..977cd2df4c 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 6911643a6b..3f697a47b2 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 0633b3fed6..ea5a51d1f5 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 0f74fb1a94..e04665f2ce 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 5f5bdb310c..bce057c9a0 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 13c08b22e1..f9e3a89968 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 1bca0d1b78..76bd4ba872 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 ca6b7b11b1..b06706ad69 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 c2af9a7086..83a43b85d5 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 22c74fc586..4b402418a8 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 f4a1dc4c11..d50d341505 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 5e858ca67c..25ee70c0b1 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 3a26e1ac66..54458cfc22 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 654cb30395..07bc0026bc 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 4d64cf7ad3..a524bebc2f 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 c34e62b656..fb6d6e156c 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 a30528c695..e7b3b9a88d 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 0984e73339..854850c185 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 767ddb1861..fb0aa85f7c 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 435399e770..125121f744 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 9da858923f..10fd627e20 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 27dfb7a93f..f68ef966fb 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 e3ad2113da..e8c02123aa 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 1d1b30e6fc..6cc12b3965 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 8299b49f7b..1b0c7e2faa 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 0f9007ea9f..8611f0845c 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 bc269c4b44..7d170c7b84 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 efe19e07e0..8d3fabd66d 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 a4923de69a..f1e6838577 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 b53ab112ce..92765a970a 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 e6d875af02..06ce88fc4f 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 a49bbeb26d..f4e58b3147 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 ba3eb28b8c..b511c02ccb 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 1cff8fb8a4..70201fcd2a 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 28ef9748b7..d1152c32e6 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 ce860ab146..737f3c81ef 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 f7dbcf9acf..104a77b552 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 028082658b..18e8cded6b 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 addcb92b92..aeb303ff05 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 ca4cfd18ab..69719e29d3 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 f99511c7db..f5f6f5949e 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 348a0c889c..ad84ade7b2 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 df9a5f8f83..ed9dd77cb2 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 190c23739e..21ee8ce5c8 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 52189c1793..3c70ce424c 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 c44a5aea3b..57c9475193 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 f2603deb35..94f5e76ff8 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 f4d9e77901..df94c5487c 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 6bd1761beb..5d7807ed03 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 b8c17da527..fdf9648f7b 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 a76b8c22ca..20b669d1ba 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 3238dce74f..fe97896b36 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 fc996c3a8e..387c076929 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 4a95974a1e..cfcb91749e 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 ca1431ecae..bc10799b42 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 2893331a18..a3cca23eec 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 369825c83a..3721ec4515 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 9009165bf5..9b047a3c55 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 6758466c70..99e9459f2b 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 83321dbf80..cf439f6b53 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 e79eb4cd58..592baf9089 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 de9f912227..a1bd42138b 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 83ad6d6a25..cd3899b9be 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 9ef0b43907..d577cbe84b 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 1f17a77194..15224ff3b0 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 a4af396157..8c3930bd27 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 0877ee0635..81a87236ee 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 2fa8966ae6..72037b6bd8 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 4000870db2..e087b61934 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 6039370a36..f9025f8914 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 5c4cf4de25..d447f54adb 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 6ff40a4a28..53bec744e3 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 976b58c8f5..7d9e697ba8 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 4494b08108..fc04ab7094 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 e4e3d1f033..a0bfdd85b1 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 3ea3494696..47f2f8cf8a 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 67f04c955f..8ae735d33f 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 f8bde9a6cf..e05bc532f5 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 2b79b20697..1aaf9bd25f 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'
+ // 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'
+ }
- const headers = await fetchHeaderRow(
- accessToken,
- spreadsheetId,
- sheetName,
- valueRender,
- dateTimeRender,
- requestId,
- logger
- )
-
- // 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 d57c19fa50..6dcdd9cdff 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 | 104 ++-----
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, 1592 insertions(+), 386 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 0000000000..3be8c2dc3b
--- /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 e8f05ecfcf..9db6eef95c 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 7dfeafb2ef..070b389313 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 c09688c99d..8cf37410ae 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 9a91b86b8e..752ebc1a9e 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 cd5ebb5301..b9b0ccc4a9 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 e6ffbaa626..8ec89923bb 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 51110916e9..d522cf9c3d 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 656d39fdde..fc3759b0e2 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 aef704f7b9..926714b98f 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 b5166630af..c120e84b42 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 14f2e73142..c3a6156905 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 7d30b63d7b..0b21032506 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 229ba26382..f8167e92ac 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 98e80f5aa3..37e0ae8d1d 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 577363b8d9..a57a7c937b 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 6512ba9580..db29c2759d 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 48e0d0deb2..c5e7878fc6 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 87cdb51a73..6ffee2355a 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 df7057fc90..1ace24c886 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 b5e2cb86f4..34da8e0327 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 4f8735826b..f238ac4f97 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 83056e8f48..b5614aec41 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 59be57cd61..8d5ee15391 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 1d37f664ab..02d8b3e5af 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 2dcf53701d..5da7026a45 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 20499ce8fc..9641f3a453 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 54265bb687..67c893fc75 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 054c7a3a2c..5d5c1d8b6f 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 f5ed5371e1..6ed1bb2e0c 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 f54caf4703..60791a36bc 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 1a4687b44f..396cfe9246 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 84d431fa42..807df76967 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 f54e72b270..7f4f7d8004 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 001184d98e..5a85cfbb4f 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 3b15d34848..989d792b6f 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 5803f85dc2..6185f120f4 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 9391b67d82..51cbe1222b 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 b79f01ebc5..9b88d48261 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 597634aeb9..d05514a883 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 da291cdccc..a2f14f109a 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 41173c1318..f1db74a3cc 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 9175de0b66..fca864c875 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 260b64f582..82c73fffa0 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 c6424865c8..0a9f9d02b7 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 7d45353e60..426da0273c 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 895ac1ff3e..f97c755da3 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 0000000000..14e24c6542
--- /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 5a090391da..046680bde4 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
+ 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,
+ })
- 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 { 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 7007053681..b3d3db8ceb 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 8c344c1575..2781fe164a 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 b69721329a..22d40d979f 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 7310a4eca9..6eb61e2261 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 0b7012c877..b6b5ed3c8a 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 9d45e677bd..61741d3c59 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 09ff717f9c..d0c0ad3e64 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 24d93fc060..e146c93950 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 c6ef9e992e..0c7174f429 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 a209db29eb..a2fb4fe4ba 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 63c230f686..0af8a82bae 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 a9d6b6ba1a..c0b4d3d535 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 1b4cd8ab3b..064669c9b8 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 3615afd089..f96bd6d352 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 42711f1fa8..3345888a6f 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 a6a15bb52f..4c156d06f9 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 65f177b1c5..5ccda1fae7 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 2e118b628d..67b1eddeb7 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 24b5eb56cf..179efc41d3 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 c440618863..34cacc6808 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 5c88744279..41bdf82569 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 08d3f5802d..ae5ae96c3e 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 1a18f8d238..3ad7532f8e 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 f31bce34ba..e7ee538559 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 375e0879b8..ca4e9408fc 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 602b60e88c..d76322d4e5 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 30c91acd22..020e350dbb 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 ca91871294..e4a507c5a7 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 d64fd05f75..1fdc15f94d 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 4642fc9e84..b6f439635b 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 eb6941cb10..ff25389fc0 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 0000000000..ae5504c7ff
--- /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 0000000000..7aef8bd794
--- /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 0000000000..445265f4f8
--- /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 0000000000..259d6094c0
--- /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 fc0e8ba3fc..ea7783aba7 100644
--- a/apps/sim/lib/audit/log.ts
+++ b/apps/sim/lib/audit/log.ts
@@ -2,215 +2,15 @@ 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'
+export type { AuditActionType, AuditResourceTypeValue } from '@/lib/audit/types'
+export { AuditAction, AuditResourceType } from '@/lib/audit/types'
+
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',
-
- // 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]
-
interface AuditLogParams {
workspaceId?: string | null
actorId: string
diff --git a/apps/sim/lib/audit/types.ts b/apps/sim/lib/audit/types.ts
new file mode 100644
index 0000000000..bc1f857f46
--- /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 98e48bd780..b08d15d743 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 eb89c876ce..f75d6d7fbc 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 233b1cbfe2..00ecfce4dc 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 5cbbcdd573..ac48a67708 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 c2a378cb76..615fcdee64 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 1a00e325d2..ada57d5064 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 5e8863ccb0..d8709e47d5 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 0b4a46d9de..a36f182738 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 0000000000..6881c060ee
--- /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 0000000000..94186db6e3
--- /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 94b9531ec1..d01cd5d359 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 5f9ca80cdd..c500f440cb 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 ae83961eab..80332d4c35 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 352f4cc904..c47d08d6b9 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 0a77701f85..cf9b74c6c0 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 0f25b32a62..f63be54314 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 a327eb44c0..6ad3ec9355 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 1b6cdc9eca..891667249f 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 9bb874a021..8905a552b5 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 8d03f2491c..aad99e364e 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 8a43548acb..27119bf180 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 afb27cd729..5d1b2d25ff 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 6d505132e3..512f41a32e 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 9bae1dd8ae..a7339cc76a 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 0000000000..d7a213f249
--- /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 5db2aad66c..13200885ba 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 f9af4ac4bc..9a06d79faf 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 6cd9c3f7f3..a27a1cf30b 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 7098c4abf7..217dd210ed 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 ef5929963e..6e7ba497ae 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 8f595d6907..7e8d734261 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 d4d0789467..dac481c590 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 1fc0122650..b03c4fb45e 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 ecb9ae7fd3..cb799237ae 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 0000000000..b746d7a527
--- /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 5fffa1121b..4f2a91d7e7 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 35290f9e71..e93bf9c73f 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 bdc67a537b..60927489ee 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 d79e5faaa5..5a747b1306 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 fd4b8eafc7..8250d2e1a3 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 f98b9f73ce..dd73b1ea1c 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 b0ff326059..bb9a87e4ad 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 851d948655..ccdda2603e 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 0000000000..dadd3fcd08
--- /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 915a235414..b198f56aa6 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 89fbc5a67f..0e6f7c3187 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 77c92d0c39..d66d2db1a1 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 c5e693a4ca..f746d2b3da 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 710acc532c..0a1a8929a3 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 0928460c00..32cf3f27be 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 16dce2da97..cfaefd63f4 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 faee69c236..d7e7693158 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 faf9895bf6..9e0cdc6637 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 af8e397fa8..ea33e518c7 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 | 61 +-
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 | 212 +++----
.../emcn/components/modal/modal.tsx | 4 +-
apps/sim/components/icons.tsx | 2 +-
.../components/access-control.tsx | 550 ++++++++++--------
.../utils/permission-check.test.ts | 4 +
.../ee/audit-logs/components/audit-logs.tsx | 298 +++++++---
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, 875 insertions(+), 523 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 69fc92e8a1..0cd2aa9dba 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 51cbe1222b..7cab684f04 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
- .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 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 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 9b88d48261..dd5c09e545 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 0000000000..2880c9eca0
--- /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 ca48abef01..2c0db1d6e1 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 7ac50861e5..d04d9481d1 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 b6f439635b..9610babe87 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 0fab587de0..06cebe3773 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 ff25389fc0..222bb1962a 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 ccb7cba760..9c1bc3fafc 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
+ : 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
+ 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
+
+ )}
+ >
+ )
- 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}
-
- )
+ 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}
-
- )
- })}
+ 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 3e2341ce0c..91fc76ebd9 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 2806cd4d31..0e4de0bf5e 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 cc636fb9df..65cc642e13 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()
@@ -313,18 +323,24 @@ export function AccessControl() {
category: 'Workflow Panel',
configKey: 'hideCopilot' as const,
},
+ {
+ id: 'hide-integrations',
+ label: 'Integrations',
+ category: 'Settings Tabs',
+ configKey: 'hideIntegrationsTab' as const,
+ },
+ {
+ id: 'hide-secrets',
+ label: 'Secrets',
+ category: 'Settings Tabs',
+ configKey: 'hideSecretsTab' as const,
+ },
{
id: 'hide-api-keys',
label: 'API Keys',
category: 'Settings Tabs',
configKey: 'hideApiKeysTab' as const,
},
- {
- id: 'hide-environment',
- label: 'Environment',
- category: 'Settings Tabs',
- configKey: 'hideEnvironmentTab' as const,
- },
{
id: 'hide-files',
label: '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}
+
+ )
+ })}
-
-
+
-
-
-
-
-
-
- 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 =
- !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'}
-
+
+
+
+
+ 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'
+ />
-
- {Object.entries(platformCategories).map(([category, features]) => (
-
-
- {category}
-
-
- {features.map((feature) => (
-
+ {
+ 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'}
+
+
+
+ {filteredCoreBlocks.length > 0 && (
+
+
+ Core Blocks
+
+
+ {filteredCoreBlocks.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}
+
+ )
+ })}
- ))}
-
+
+ )}
+ {filteredToolBlocks.length > 0 && (
+
+
+ Tools
+
+
+ {filteredToolBlocks.map((block) => {
+ const BlockIcon = block.icon
+ const checkboxId = `block-${block.type}`
+ return (
+
+ toggleIntegration(block.type)}
+ />
+
+ {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})
- )}
-
+
+
+
+ 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}
+
+ )}
+ {metadataEntries.map(([key, value]) => (
+
+
+ {formatMetadataLabel(key)}
+
+
{renderMetadataValue(value)}
+
+ ))}
- {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}
)}
@@ -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 2dfa1592ea..37c20ffb7f 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 a6946772c9..fa28f3d85b 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 c30fbff6c3..3900bb9d38 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 7b329a2fd1..7a0a510bf2 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 b66444d43e..8e3820c778 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 52a77d4da5..c76ad7d1a8 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 40d4a0ebdf..f4f5d4c9f0 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 e05bc532f5..b071c71851 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 8d9d690678..7913e730f4 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 c7ff404e8c..69e33c5890 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}": [