From f2ef7d7d2e97893f61ac70fcabdf869748a1e0b7 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 11 Apr 2026 11:27:53 -0700 Subject: [PATCH 01/17] 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 --- apps/sim/app/api/audit-logs/route.ts | 71 +++++ apps/sim/app/api/v1/admin/audit-logs/route.ts | 37 +-- apps/sim/app/api/v1/audit-logs/query.ts | 141 +++++++++ apps/sim/app/api/v1/audit-logs/route.ts | 106 ++----- .../settings/[section]/settings.tsx | 6 + .../[workspaceId]/settings/navigation.ts | 10 + .../components/audit-logs-skeleton.tsx | 27 ++ .../ee/audit-logs/components/audit-logs.tsx | 297 ++++++++++++++++++ apps/sim/ee/audit-logs/hooks/audit-logs.ts | 58 ++++ 9 files changed, 647 insertions(+), 106 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/hooks/audit-logs.ts diff --git a/apps/sim/app/api/audit-logs/route.ts b/apps/sim/app/api/audit-logs/route.ts new file mode 100644 index 00000000000..3be8c2dc3b6 --- /dev/null +++ b/apps/sim/app/api/audit-logs/route.ts @@ -0,0 +1,71 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { validateEnterpriseAuditAccess } from '@/app/api/v1/audit-logs/auth' +import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format' +import { + buildFilterConditions, + buildOrgScopeCondition, + queryAuditLogs, +} from '@/app/api/v1/audit-logs/query' + +const logger = createLogger('AuditLogsAPI') + +export const dynamic = 'force-dynamic' + +export async function GET(request: Request) { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const authResult = await validateEnterpriseAuditAccess(session.user.id) + if (!authResult.success) { + return authResult.response + } + + const { orgMemberIds } = authResult.context + + const { searchParams } = new URL(request.url) + const search = searchParams.get('search')?.trim() || undefined + const startDate = searchParams.get('startDate') || undefined + const endDate = searchParams.get('endDate') || undefined + const includeDeparted = searchParams.get('includeDeparted') === 'true' + const limit = Math.min(Math.max(Number(searchParams.get('limit')) || 50, 1), 100) + const cursor = searchParams.get('cursor') || undefined + + if (startDate && Number.isNaN(Date.parse(startDate))) { + return NextResponse.json({ error: 'Invalid startDate format' }, { status: 400 }) + } + if (endDate && Number.isNaN(Date.parse(endDate))) { + return NextResponse.json({ error: 'Invalid endDate format' }, { status: 400 }) + } + + const scopeCondition = await buildOrgScopeCondition(orgMemberIds, includeDeparted) + const filterConditions = buildFilterConditions({ + action: searchParams.get('action') || undefined, + resourceType: searchParams.get('resourceType') || undefined, + actorId: searchParams.get('actorId') || undefined, + search, + startDate, + endDate, + }) + + const { data, nextCursor } = await queryAuditLogs( + [scopeCondition, ...filterConditions], + limit, + cursor + ) + + return NextResponse.json({ + success: true, + data: data.map(formatAuditLogEntry), + nextCursor, + }) + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Unknown error' + logger.error('Audit logs fetch error', { error: message }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/v1/admin/audit-logs/route.ts b/apps/sim/app/api/v1/admin/audit-logs/route.ts index 895ac1ff3e2..f97c755da33 100644 --- a/apps/sim/app/api/v1/admin/audit-logs/route.ts +++ b/apps/sim/app/api/v1/admin/audit-logs/route.ts @@ -21,7 +21,7 @@ import { db } from '@sim/db' import { auditLog } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, count, desc, eq, gte, lte, type SQL } from 'drizzle-orm' +import { and, count, desc } from 'drizzle-orm' import { withAdminAuth } from '@/app/api/v1/admin/middleware' import { badRequestResponse, @@ -34,6 +34,7 @@ import { parsePaginationParams, toAdminAuditLog, } from '@/app/api/v1/admin/types' +import { buildFilterConditions } from '@/app/api/v1/audit-logs/query' const logger = createLogger('AdminAuditLogsAPI') @@ -41,33 +42,27 @@ export const GET = withAdminAuth(async (request) => { const url = new URL(request.url) const { limit, offset } = parsePaginationParams(url) - const actionFilter = url.searchParams.get('action') - const resourceTypeFilter = url.searchParams.get('resourceType') - const resourceIdFilter = url.searchParams.get('resourceId') - const workspaceIdFilter = url.searchParams.get('workspaceId') - const actorIdFilter = url.searchParams.get('actorId') - const actorEmailFilter = url.searchParams.get('actorEmail') - const startDateFilter = url.searchParams.get('startDate') - const endDateFilter = url.searchParams.get('endDate') + const startDate = url.searchParams.get('startDate') || undefined + const endDate = url.searchParams.get('endDate') || undefined - if (startDateFilter && Number.isNaN(Date.parse(startDateFilter))) { + if (startDate && Number.isNaN(Date.parse(startDate))) { return badRequestResponse('Invalid startDate format. Use ISO 8601.') } - if (endDateFilter && Number.isNaN(Date.parse(endDateFilter))) { + if (endDate && Number.isNaN(Date.parse(endDate))) { return badRequestResponse('Invalid endDate format. Use ISO 8601.') } try { - const conditions: SQL[] = [] - - if (actionFilter) conditions.push(eq(auditLog.action, actionFilter)) - if (resourceTypeFilter) conditions.push(eq(auditLog.resourceType, resourceTypeFilter)) - if (resourceIdFilter) conditions.push(eq(auditLog.resourceId, resourceIdFilter)) - if (workspaceIdFilter) conditions.push(eq(auditLog.workspaceId, workspaceIdFilter)) - if (actorIdFilter) conditions.push(eq(auditLog.actorId, actorIdFilter)) - if (actorEmailFilter) conditions.push(eq(auditLog.actorEmail, actorEmailFilter)) - if (startDateFilter) conditions.push(gte(auditLog.createdAt, new Date(startDateFilter))) - if (endDateFilter) conditions.push(lte(auditLog.createdAt, new Date(endDateFilter))) + const conditions = buildFilterConditions({ + action: url.searchParams.get('action') || undefined, + resourceType: url.searchParams.get('resourceType') || undefined, + resourceId: url.searchParams.get('resourceId') || undefined, + workspaceId: url.searchParams.get('workspaceId') || undefined, + actorId: url.searchParams.get('actorId') || undefined, + actorEmail: url.searchParams.get('actorEmail') || undefined, + startDate, + endDate, + }) const whereClause = conditions.length > 0 ? and(...conditions) : undefined diff --git a/apps/sim/app/api/v1/audit-logs/query.ts b/apps/sim/app/api/v1/audit-logs/query.ts new file mode 100644 index 00000000000..e127f406333 --- /dev/null +++ b/apps/sim/app/api/v1/audit-logs/query.ts @@ -0,0 +1,141 @@ +import { db } from '@sim/db' +import { auditLog, workspace } from '@sim/db/schema' +import { and, desc, eq, gte, ilike, inArray, lt, lte, or, type SQL } from 'drizzle-orm' +import type { InferSelectModel } from 'drizzle-orm' + +type DbAuditLog = InferSelectModel + +interface CursorData { + createdAt: string + id: string +} + +export function encodeCursor(data: CursorData): string { + return Buffer.from(JSON.stringify(data)).toString('base64') +} + +export 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 searchTerm = `%${params.search}%` + 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 (!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) +} + +export function buildCursorCondition(cursor: string): SQL | null { + const cursorData = decodeCursor(cursor) + if (!cursorData?.createdAt || !cursorData.id) return null + + const cursorDate = new Date(cursorData.createdAt) + if (Number.isNaN(cursorDate.getTime())) return null + + return or( + lt(auditLog.createdAt, cursorDate), + and(eq(auditLog.createdAt, cursorDate), lt(auditLog.id, cursorData.id)) + )! +} + +interface CursorPaginatedResult { + data: DbAuditLog[] + nextCursor?: string +} + +export async function queryAuditLogs( + conditions: SQL[], + limit: number, + cursor?: string +): Promise { + const allConditions = [...conditions] + + if (cursor) { + const cursorCondition = buildCursorCondition(cursor) + if (cursorCondition) allConditions.push(cursorCondition) + } + + const rows = await db + .select() + .from(auditLog) + .where(allConditions.length > 0 ? and(...allConditions) : undefined) + .orderBy(desc(auditLog.createdAt), desc(auditLog.id)) + .limit(limit + 1) + + const hasMore = rows.length > limit + const data = rows.slice(0, limit) + + let nextCursor: string | undefined + if (hasMore && data.length > 0) { + const last = data[data.length - 1] + nextCursor = encodeCursor({ + createdAt: last.createdAt.toISOString(), + id: last.id, + }) + } + + return { data, nextCursor } +} diff --git a/apps/sim/app/api/v1/audit-logs/route.ts b/apps/sim/app/api/v1/audit-logs/route.ts index 5a090391da4..046680bde44 100644 --- a/apps/sim/app/api/v1/audit-logs/route.ts +++ b/apps/sim/app/api/v1/audit-logs/route.ts @@ -19,15 +19,17 @@ * Response: { data: AuditLogEntry[], nextCursor?: string, limits: UserLimits } */ -import { db } from '@sim/db' -import { auditLog, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, desc, eq, gte, inArray, lt, lte, or, type SQL } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { generateId } from '@/lib/core/utils/uuid' import { validateEnterpriseAuditAccess } from '@/app/api/v1/audit-logs/auth' import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format' +import { + buildFilterConditions, + buildOrgScopeCondition, + queryAuditLogs, +} from '@/app/api/v1/audit-logs/query' import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta' import { checkRateLimit, createRateLimitResponse } from '@/app/api/v1/middleware' @@ -57,23 +59,6 @@ const QueryParamsSchema = z.object({ cursor: z.string().optional(), }) -interface CursorData { - createdAt: string - id: string -} - -function encodeCursor(data: CursorData): string { - return Buffer.from(JSON.stringify(data)).toString('base64') -} - -function decodeCursor(cursor: string): CursorData | null { - try { - return JSON.parse(Buffer.from(cursor, 'base64').toString()) - } catch { - return null - } -} - export async function GET(request: NextRequest) { const requestId = generateId().slice(0, 8) @@ -112,71 +97,22 @@ export async function GET(request: NextRequest) { ) } - let scopeCondition: SQL - - if (params.includeDeparted) { - const orgWorkspaces = await db - .select({ id: workspace.id }) - .from(workspace) - .where(inArray(workspace.ownerId, orgMemberIds)) - - const orgWorkspaceIds = orgWorkspaces.map((w) => w.id) - - if (orgWorkspaceIds.length > 0) { - scopeCondition = or( - inArray(auditLog.actorId, orgMemberIds), - inArray(auditLog.workspaceId, orgWorkspaceIds) - )! - } else { - scopeCondition = inArray(auditLog.actorId, orgMemberIds) - } - } else { - scopeCondition = inArray(auditLog.actorId, orgMemberIds) - } - - const conditions: SQL[] = [scopeCondition] - - if (params.action) conditions.push(eq(auditLog.action, params.action)) - if (params.resourceType) conditions.push(eq(auditLog.resourceType, params.resourceType)) - if (params.resourceId) conditions.push(eq(auditLog.resourceId, params.resourceId)) - if (params.workspaceId) conditions.push(eq(auditLog.workspaceId, params.workspaceId)) - if (params.actorId) conditions.push(eq(auditLog.actorId, params.actorId)) - if (params.startDate) conditions.push(gte(auditLog.createdAt, new Date(params.startDate))) - if (params.endDate) conditions.push(lte(auditLog.createdAt, new Date(params.endDate))) - - if (params.cursor) { - const cursorData = decodeCursor(params.cursor) - if (cursorData?.createdAt && cursorData.id) { - const cursorDate = new Date(cursorData.createdAt) - if (!Number.isNaN(cursorDate.getTime())) { - conditions.push( - or( - lt(auditLog.createdAt, cursorDate), - and(eq(auditLog.createdAt, cursorDate), lt(auditLog.id, cursorData.id)) - )! - ) - } - } - } - - const rows = await db - .select() - .from(auditLog) - .where(and(...conditions)) - .orderBy(desc(auditLog.createdAt), desc(auditLog.id)) - .limit(params.limit + 1) - - const hasMore = rows.length > params.limit - const data = rows.slice(0, params.limit) - - let nextCursor: string | undefined - if (hasMore && data.length > 0) { - const last = data[data.length - 1] - nextCursor = encodeCursor({ - createdAt: last.createdAt.toISOString(), - id: last.id, - }) - } + const scopeCondition = await buildOrgScopeCondition(orgMemberIds, params.includeDeparted) + const filterConditions = buildFilterConditions({ + action: params.action, + resourceType: params.resourceType, + resourceId: params.resourceId, + workspaceId: params.workspaceId, + actorId: params.actorId, + startDate: params.startDate, + endDate: params.endDate, + }) + + const { data, nextCursor } = await queryAuditLogs( + [scopeCondition, ...filterConditions], + params.limit, + params.cursor + ) const formattedLogs = data.map(formatAuditLogEntry) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx index 4642fc9e843..b680432f644 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx @@ -9,6 +9,7 @@ import { useSession } from '@/lib/auth/auth-client' 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' +import { AuditLogsSkeleton } from '@/ee/audit-logs/components/audit-logs-skeleton' import { BYOKSkeleton } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton' import { CopilotSkeleton } from '@/app/workspace/[workspaceId]/settings/components/copilot/copilot-skeleton' import { CredentialSetsSkeleton } from '@/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets-skeleton' @@ -153,6 +154,10 @@ const AccessControl = dynamic( () => import('@/ee/access-control/components/access-control').then((m) => m.AccessControl), { loading: () => } ) +const AuditLogs = dynamic( + () => import('@/ee/audit-logs/components/audit-logs').then((m) => m.AuditLogs), + { loading: () => } +) const SSO = dynamic(() => import('@/ee/sso/components/sso-settings').then((m) => m.SSO), { loading: () => , }) @@ -201,6 +206,7 @@ export function SettingsPage({ section }: SettingsPageProps) { {/* {effectiveSection === 'template-profile' && } */} {effectiveSection === 'credential-sets' && } {effectiveSection === 'access-control' && } + {effectiveSection === 'audit-logs' && } {effectiveSection === 'apikeys' && } {isBillingEnabled && effectiveSection === 'subscription' && } {isBillingEnabled && effectiveSection === 'team' && } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts index eb6941cb10b..ff25389fc0c 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts @@ -1,5 +1,6 @@ import { Card, + ClipboardList, Connections, HexSimple, Key, @@ -27,6 +28,7 @@ export type SettingsSection = | 'template-profile' | 'credential-sets' | 'access-control' + | 'audit-logs' | 'apikeys' | 'byok' | 'subscription' @@ -97,6 +99,14 @@ export const allNavigationItems: NavigationItem[] = [ requiresEnterprise: true, selfHostedOverride: isAccessControlEnabled, }, + { + id: 'audit-logs', + label: 'Audit Logs', + icon: ClipboardList, + section: 'enterprise', + requiresHosted: true, + requiresEnterprise: true, + }, { id: 'subscription', label: 'Subscription', diff --git a/apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx b/apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx new file mode 100644 index 00000000000..a425b445fe0 --- /dev/null +++ b/apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx @@ -0,0 +1,27 @@ +import { Skeleton } from '@/components/emcn' + +export function AuditLogsSkeleton() { + return ( +
+
+ + + +
+
+ + + + +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+ + + + +
+ ))} +
+ ) +} diff --git a/apps/sim/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx new file mode 100644 index 00000000000..930d77281f5 --- /dev/null +++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx @@ -0,0 +1,297 @@ +'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 { formatDateTime } from '@/lib/core/utils/formatting' +import { cn } from '@/lib/utils' +import { type AuditLogFilters, useAuditLogs } from '@/ee/audit-logs/hooks/audit-logs' +import type { EnterpriseAuditLogEntry } from '@/app/api/v1/audit-logs/format' + +const logger = createLogger('AuditLogs') + +const RESOURCE_TYPE_OPTIONS: ComboboxOption[] = [ + { label: 'All Types', value: '' }, + { label: 'API Key', value: 'api_key' }, + { label: 'Billing', value: 'billing' }, + { label: 'BYOK Key', value: 'byok_key' }, + { label: 'Chat', value: 'chat' }, + { label: 'Connector', value: 'connector' }, + { label: 'Credential Set', value: 'credential_set' }, + { label: 'Custom Tool', value: 'custom_tool' }, + { label: 'Document', value: 'document' }, + { label: 'Environment', value: 'environment' }, + { label: 'File', value: 'file' }, + { label: 'Folder', value: 'folder' }, + { label: 'Form', value: 'form' }, + { label: 'Knowledge Base', value: 'knowledge_base' }, + { label: 'MCP Server', value: 'mcp_server' }, + { label: 'Notification', value: 'notification' }, + { label: 'OAuth', value: 'oauth' }, + { label: 'Organization', value: 'organization' }, + { label: 'Password', value: 'password' }, + { label: 'Permission Group', value: 'permission_group' }, + { label: 'Schedule', value: 'schedule' }, + { label: 'Skill', value: 'skill' }, + { label: 'Table', value: 'table' }, + { label: 'Template', value: 'template' }, + { label: 'Webhook', value: 'webhook' }, + { label: 'Workflow', value: 'workflow' }, + { label: 'Workspace', value: 'workspace' }, +] + +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 ( +
+ + {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 && Object.keys(entry.metadata as Record).length > 0 && ( +
+ Details +
+                {JSON.stringify(entry.metadata, null, 2)}
+              
+
+ )} +
+ )} +
+ ) +} + +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(() => { + debounceRef.current = setTimeout(() => { + setDebouncedSearch(searchTerm.trim()) + }, 300) + return () => { + if (debounceRef.current) clearTimeout(debounceRef.current) + } + }, [searchTerm]) + + 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 && ( +
+ +
+ )} +
+ )} +
+
+ ) +} diff --git a/apps/sim/ee/audit-logs/hooks/audit-logs.ts b/apps/sim/ee/audit-logs/hooks/audit-logs.ts new file mode 100644 index 00000000000..259d6094c0d --- /dev/null +++ b/apps/sim/ee/audit-logs/hooks/audit-logs.ts @@ -0,0 +1,58 @@ +import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query' +import type { EnterpriseAuditLogEntry } from '@/app/api/v1/audit-logs/format' + +export const auditLogKeys = { + all: ['audit-logs'] as const, + lists: () => [...auditLogKeys.all, 'list'] as const, + list: (filters: AuditLogFilters) => [...auditLogKeys.lists(), filters] as const, +} + +export interface AuditLogFilters { + search?: string + action?: string + resourceType?: string + actorId?: string + startDate?: string + endDate?: string +} + +interface AuditLogPage { + success: boolean + data: EnterpriseAuditLogEntry[] + nextCursor?: string +} + +async function fetchAuditLogs( + filters: AuditLogFilters, + cursor?: string, + signal?: AbortSignal +): Promise { + const params = new URLSearchParams() + params.set('limit', '50') + if (filters.search) params.set('search', filters.search) + if (filters.action) params.set('action', filters.action) + if (filters.resourceType) params.set('resourceType', filters.resourceType) + if (filters.actorId) params.set('actorId', filters.actorId) + if (filters.startDate) params.set('startDate', filters.startDate) + if (filters.endDate) params.set('endDate', filters.endDate) + if (cursor) params.set('cursor', cursor) + + const response = await fetch(`/api/audit-logs?${params.toString()}`, { signal }) + if (!response.ok) { + const body = await response.json().catch(() => ({})) + throw new Error(body.error || `Failed to fetch audit logs: ${response.status}`) + } + return response.json() +} + +export function useAuditLogs(filters: AuditLogFilters, enabled = true) { + return useInfiniteQuery({ + queryKey: auditLogKeys.list(filters), + queryFn: ({ pageParam, signal }) => fetchAuditLogs(filters, pageParam, signal), + initialPageParam: undefined as string | undefined, + getNextPageParam: (lastPage) => lastPage.nextCursor, + enabled, + staleTime: 30 * 1000, + placeholderData: keepPreviousData, + }) +} From 1d13c4da2989230e43cd262b31219f23b57ef3b0 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 11 Apr 2026 11:30:28 -0700 Subject: [PATCH 02/17] lint --- apps/sim/app/api/v1/audit-logs/query.ts | 2 +- .../[workspaceId]/settings/[section]/settings.tsx | 2 +- .../ee/audit-logs/components/audit-logs-skeleton.tsx | 2 +- apps/sim/ee/audit-logs/components/audit-logs.tsx | 12 ++++-------- bun.lock | 1 + 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/api/v1/audit-logs/query.ts b/apps/sim/app/api/v1/audit-logs/query.ts index e127f406333..62c7bb3167b 100644 --- a/apps/sim/app/api/v1/audit-logs/query.ts +++ b/apps/sim/app/api/v1/audit-logs/query.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { auditLog, workspace } from '@sim/db/schema' -import { and, desc, eq, gte, ilike, inArray, lt, lte, or, type SQL } from 'drizzle-orm' import type { InferSelectModel } from 'drizzle-orm' +import { and, desc, eq, gte, ilike, inArray, lt, lte, or, type SQL } from 'drizzle-orm' type DbAuditLog = InferSelectModel diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx index b680432f644..b6f439635b9 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx @@ -9,7 +9,6 @@ import { useSession } from '@/lib/auth/auth-client' 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' -import { AuditLogsSkeleton } from '@/ee/audit-logs/components/audit-logs-skeleton' import { BYOKSkeleton } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton' import { CopilotSkeleton } from '@/app/workspace/[workspaceId]/settings/components/copilot/copilot-skeleton' import { CredentialSetsSkeleton } from '@/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets-skeleton' @@ -28,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. diff --git a/apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx b/apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx index a425b445fe0..6ede97463b4 100644 --- a/apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx +++ b/apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx @@ -8,7 +8,7 @@ export function AuditLogsSkeleton() { -
+
diff --git a/apps/sim/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx index 930d77281f5..d3a759b5f12 100644 --- a/apps/sim/ee/audit-logs/components/audit-logs.tsx +++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx @@ -7,8 +7,8 @@ import { Badge, Button, Combobox, type ComboboxOption, Skeleton } from '@/compon import { Input } from '@/components/ui' import { formatDateTime } from '@/lib/core/utils/formatting' import { cn } from '@/lib/utils' -import { type AuditLogFilters, useAuditLogs } from '@/ee/audit-logs/hooks/audit-logs' import type { EnterpriseAuditLogEntry } from '@/app/api/v1/audit-logs/format' +import { type AuditLogFilters, useAuditLogs } from '@/ee/audit-logs/hooks/audit-logs' const logger = createLogger('AuditLogs') @@ -90,7 +90,7 @@ function AuditLogRow({ entry }: AuditLogRowProps) { const timestamp = formatDateTime(new Date(entry.createdAt)) return ( -
+
-
+
Timestamp @@ -280,11 +280,7 @@ export function AuditLogs() { ))} {hasNextPage && (
-
diff --git a/bun.lock b/bun.lock index f8bde9a6cf3..e05bc532f5e 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "simstudio", From 3eb6b3038564acf2b52f6267529201a895bff3aa Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 11 Apr 2026 11:41:01 -0700 Subject: [PATCH 03/17] fix(ee): fix build error and address PR review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/sim/app/api/v1/audit-logs/query.ts | 6 +++++- apps/sim/ee/audit-logs/components/audit-logs.tsx | 8 +++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/api/v1/audit-logs/query.ts b/apps/sim/app/api/v1/audit-logs/query.ts index 62c7bb3167b..a671f6065cb 100644 --- a/apps/sim/app/api/v1/audit-logs/query.ts +++ b/apps/sim/app/api/v1/audit-logs/query.ts @@ -1,7 +1,7 @@ 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 } from 'drizzle-orm' +import { and, desc, eq, gte, ilike, inArray, lt, lte, or, sql, type SQL } from 'drizzle-orm' type DbAuditLog = InferSelectModel @@ -67,6 +67,10 @@ export async function buildOrgScopeCondition( orgMemberIds: string[], includeDeparted: boolean ): Promise> { + if (orgMemberIds.length === 0) { + return sql`1 = 0` + } + if (!includeDeparted) { return inArray(auditLog.actorId, orgMemberIds) } diff --git a/apps/sim/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx index d3a759b5f12..0f41abad401 100644 --- a/apps/sim/ee/audit-logs/components/audit-logs.tsx +++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx @@ -6,7 +6,7 @@ import { RefreshCw, Search } from 'lucide-react' import { Badge, Button, Combobox, type ComboboxOption, Skeleton } from '@/components/emcn' import { Input } from '@/components/ui' import { formatDateTime } from '@/lib/core/utils/formatting' -import { cn } from '@/lib/utils' +import { cn } from '@/lib/core/utils/cn' import type { EnterpriseAuditLogEntry } from '@/app/api/v1/audit-logs/format' import { type AuditLogFilters, useAuditLogs } from '@/ee/audit-logs/hooks/audit-logs' @@ -163,13 +163,15 @@ export function AuditLogs() { const debounceRef = useRef | null>(null) useEffect(() => { + const trimmed = searchTerm.trim() + if (trimmed === debouncedSearch) return debounceRef.current = setTimeout(() => { - setDebouncedSearch(searchTerm.trim()) + setDebouncedSearch(trimmed) }, 300) return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } - }, [searchTerm]) + }, [searchTerm, debouncedSearch]) const filters = useMemo(() => { return { From 36a1aa4f43cac7c9227205b59dfaf1db0710e7b1 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 11 Apr 2026 11:43:12 -0700 Subject: [PATCH 04/17] lint --- apps/sim/app/api/v1/audit-logs/query.ts | 2 +- apps/sim/ee/audit-logs/components/audit-logs.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/api/v1/audit-logs/query.ts b/apps/sim/app/api/v1/audit-logs/query.ts index a671f6065cb..9fe7c8174cf 100644 --- a/apps/sim/app/api/v1/audit-logs/query.ts +++ b/apps/sim/app/api/v1/audit-logs/query.ts @@ -1,7 +1,7 @@ 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, sql, type SQL } from 'drizzle-orm' +import { and, desc, eq, gte, ilike, inArray, lt, lte, or, type SQL, sql } from 'drizzle-orm' type DbAuditLog = InferSelectModel diff --git a/apps/sim/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx index 0f41abad401..be265a8e1d5 100644 --- a/apps/sim/ee/audit-logs/components/audit-logs.tsx +++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx @@ -5,8 +5,8 @@ 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 { formatDateTime } from '@/lib/core/utils/formatting' 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 { type AuditLogFilters, useAuditLogs } from '@/ee/audit-logs/hooks/audit-logs' From b9ea27d7fad498c727ec7da44d995a99f700078c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 11 Apr 2026 11:52:34 -0700 Subject: [PATCH 05/17] 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 --- apps/sim/ee/audit-logs/components/audit-logs.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx index be265a8e1d5..ba7aa0d3214 100644 --- a/apps/sim/ee/audit-logs/components/audit-logs.tsx +++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx @@ -141,14 +141,14 @@ function AuditLogRow({ entry }: AuditLogRowProps) { {entry.description}
)} - {entry.metadata && Object.keys(entry.metadata as Record).length > 0 && ( + {entry.metadata != null && Object.keys(entry.metadata as Record).length > 0 ? (
Details
                 {JSON.stringify(entry.metadata, null, 2)}
               
- )} + ) : null}
)}
From 01dfe81d6f82cec85f26f59073cad2a42b618fc0 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 11 Apr 2026 11:54:39 -0700 Subject: [PATCH 06/17] fix(ee): align skeleton filter width with actual component layout Co-Authored-By: Claude Opus 4.6 --- apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx b/apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx index 6ede97463b4..ae5504c7ffc 100644 --- a/apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx +++ b/apps/sim/ee/audit-logs/components/audit-logs-skeleton.tsx @@ -6,7 +6,7 @@ export function AuditLogsSkeleton() {
- +
From bc4788a2451c019d412bd42e5f8c9d4eb33e4376 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 11 Apr 2026 11:56:31 -0700 Subject: [PATCH 07/17] lint --- apps/sim/ee/audit-logs/components/audit-logs.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/sim/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx index ba7aa0d3214..d50bdde8603 100644 --- a/apps/sim/ee/audit-logs/components/audit-logs.tsx +++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx @@ -141,7 +141,8 @@ function AuditLogRow({ entry }: AuditLogRowProps) { {entry.description}
)} - {entry.metadata != null && Object.keys(entry.metadata as Record).length > 0 ? ( + {entry.metadata != null && + Object.keys(entry.metadata as Record).length > 0 ? (
Details

From a325138b06d5a0f77b7367582f404f0655ffc764 Mon Sep 17 00:00:00 2001
From: Waleed Latif 
Date: Sat, 11 Apr 2026 12:11:09 -0700
Subject: [PATCH 08/17] 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 
---
 .../sim/app/api/auth/forget-password/route.ts | 22 ++++++++
 apps/sim/app/api/credentials/[id]/route.ts    | 55 +++++++++++++++++++
 apps/sim/app/api/credentials/route.ts         | 16 ++++++
 apps/sim/app/api/schedules/[id]/route.ts      | 48 ++++++++++------
 apps/sim/app/api/schedules/route.ts           | 18 ++++++
 .../ee/audit-logs/components/audit-logs.tsx   |  1 +
 apps/sim/lib/audit/log.ts                     |  6 ++
 apps/sim/lib/auth/auth.ts                     |  2 +-
 8 files changed, 150 insertions(+), 18 deletions(-)

diff --git a/apps/sim/app/api/auth/forget-password/route.ts b/apps/sim/app/api/auth/forget-password/route.ts
index e8f05ecfcf1..6311b754154 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,24 @@ 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,
+        description: 'Password reset requested',
+        request,
+      })
+    }
+
     return NextResponse.json({ success: true })
   } catch (error) {
     logger.error('Error requesting password reset:', { error })
diff --git a/apps/sim/app/api/credentials/[id]/route.ts b/apps/sim/app/api/credentials/[id]/route.ts
index 14f2e73142b..302f471b876 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,21 @@ 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,
+      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 +265,18 @@ export async function DELETE(
         { groups: { workspace: access.credential.workspaceId } }
       )
 
+      recordAudit({
+        workspaceId: access.credential.workspaceId,
+        actorId: session.user.id,
+        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 +330,18 @@ export async function DELETE(
         { groups: { workspace: access.credential.workspaceId } }
       )
 
+      recordAudit({
+        workspaceId: access.credential.workspaceId,
+        actorId: session.user.id,
+        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 +358,21 @@ export async function DELETE(
       { groups: { workspace: access.credential.workspaceId } }
     )
 
+    recordAudit({
+      workspaceId: access.credential.workspaceId,
+      actorId: session.user.id,
+      action: AuditAction.CREDENTIAL_DELETED,
+      resourceType: AuditResourceType.CREDENTIAL,
+      resourceId: id,
+      resourceName: access.credential.displayName,
+      description: `Deleted ${access.credential.type} credential "${access.credential.displayName}"`,
+      metadata: {
+        credentialType: access.credential.type,
+        providerId: access.credential.providerId,
+      },
+      request,
+    })
+
     return NextResponse.json({ success: true }, { status: 200 })
   } catch (error) {
     logger.error('Failed to delete credential', error)
diff --git a/apps/sim/app/api/credentials/route.ts b/apps/sim/app/api/credentials/route.ts
index 7d30b63d7b4..3d8877cc49e 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,21 @@ export async function POST(request: NextRequest) {
       }
     )
 
+    recordAudit({
+      workspaceId,
+      actorId: session.user.id,
+      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/schedules/[id]/route.ts b/apps/sim/app/api/schedules/[id]/route.ts
index 597634aeb9d..3236bba1047 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)))
@@ -147,10 +149,13 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
         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,
       })
 
@@ -207,10 +212,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
         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,
       })
 
@@ -249,10 +256,14 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
       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 +300,16 @@ export async function DELETE(
     recordAudit({
       workspaceId,
       actorId: session.user.id,
-      action: AuditAction.SCHEDULE_UPDATED,
+      action: AuditAction.SCHEDULE_DELETED,
       resourceType: AuditResourceType.SCHEDULE,
       resourceId: scheduleId,
-      actorName: session.user.name ?? undefined,
-      actorEmail: session.user.email ?? undefined,
-      description: `Deleted ${schedule.sourceType === 'job' ? 'job' : 'schedule'} ${scheduleId}`,
-      metadata: {},
+      resourceName: schedule.jobTitle ?? undefined,
+      description: `Deleted ${schedule.sourceType === 'job' ? 'job' : 'schedule'} "${schedule.jobTitle ?? scheduleId}"`,
+      metadata: {
+        sourceType: schedule.sourceType,
+        cronExpression: schedule.cronExpression,
+        timezone: schedule.timezone,
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/schedules/route.ts b/apps/sim/app/api/schedules/route.ts
index da291cdcccc..b30b704882f 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,23 @@ export async function POST(req: NextRequest) {
       lifecycle,
     })
 
+    recordAudit({
+      workspaceId,
+      actorId: session.user.id,
+      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/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx
index d50bdde8603..6cf59d98f26 100644
--- a/apps/sim/ee/audit-logs/components/audit-logs.tsx
+++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx
@@ -19,6 +19,7 @@ const RESOURCE_TYPE_OPTIONS: ComboboxOption[] = [
   { label: 'BYOK Key', value: 'byok_key' },
   { label: 'Chat', value: 'chat' },
   { label: 'Connector', value: 'connector' },
+  { label: 'Credential', value: 'credential' },
   { label: 'Credential Set', value: 'credential_set' },
   { label: 'Custom Tool', value: 'custom_tool' },
   { label: 'Document', value: 'document' },
diff --git a/apps/sim/lib/audit/log.ts b/apps/sim/lib/audit/log.ts
index fc0e8ba3fc6..394b6f1cacf 100644
--- a/apps/sim/lib/audit/log.ts
+++ b/apps/sim/lib/audit/log.ts
@@ -108,10 +108,13 @@ export const AuditAction = {
 
   // 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
@@ -139,7 +142,9 @@ export const AuditAction = {
   SKILL_DELETED: 'skill.deleted',
 
   // Schedules
+  SCHEDULE_CREATED: 'schedule.created',
   SCHEDULE_UPDATED: 'schedule.updated',
+  SCHEDULE_DELETED: 'schedule.deleted',
 
   // Tables
   TABLE_CREATED: 'table.created',
@@ -186,6 +191,7 @@ export const AuditResourceType = {
   BYOK_KEY: 'byok_key',
   CHAT: 'chat',
   CONNECTOR: 'connector',
+  CREDENTIAL: 'credential',
   CREDENTIAL_SET: 'credential_set',
   CUSTOM_TOOL: 'custom_tool',
   DOCUMENT: 'document',
diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts
index 98e48bd7803..7fa61bf1bdd 100644
--- a/apps/sim/lib/auth/auth.ts
+++ b/apps/sim/lib/auth/auth.ts
@@ -707,7 +707,7 @@ export const auth = betterAuth({
         actorEmail: resetUser.email,
         action: AuditAction.PASSWORD_RESET,
         resourceType: AuditResourceType.PASSWORD,
-        description: 'Password reset completed',
+        description: `Password reset completed for ${resetUser.email}`,
       })
     },
   },

From 18352d906627f60550996ff4ce1b87b5c408d8f0 Mon Sep 17 00:00:00 2001
From: Waleed Latif 
Date: Sat, 11 Apr 2026 12:15:30 -0700
Subject: [PATCH 09/17] 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 
---
 apps/sim/app/api/auth/forget-password/route.ts | 4 +++-
 apps/sim/app/api/credentials/[id]/route.ts     | 8 ++++++++
 apps/sim/app/api/credentials/route.ts          | 2 ++
 apps/sim/app/api/schedules/[id]/route.ts       | 8 ++++++++
 apps/sim/app/api/schedules/route.ts            | 2 ++
 5 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/apps/sim/app/api/auth/forget-password/route.ts b/apps/sim/app/api/auth/forget-password/route.ts
index 6311b754154..9db6eef95c9 100644
--- a/apps/sim/app/api/auth/forget-password/route.ts
+++ b/apps/sim/app/api/auth/forget-password/route.ts
@@ -68,7 +68,9 @@ export async function POST(request: NextRequest) {
         actorEmail: existingUser.email,
         action: AuditAction.PASSWORD_RESET_REQUESTED,
         resourceType: AuditResourceType.PASSWORD,
-        description: 'Password reset requested',
+        resourceId: existingUser.id,
+        resourceName: existingUser.email ?? undefined,
+        description: `Password reset requested for ${existingUser.email}`,
         request,
       })
     }
diff --git a/apps/sim/app/api/credentials/[id]/route.ts b/apps/sim/app/api/credentials/[id]/route.ts
index 302f471b876..c3a61569051 100644
--- a/apps/sim/app/api/credentials/[id]/route.ts
+++ b/apps/sim/app/api/credentials/[id]/route.ts
@@ -170,6 +170,8 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
     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,
@@ -268,6 +270,8 @@ export async function DELETE(
       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,
@@ -333,6 +337,8 @@ export async function DELETE(
       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,
@@ -361,6 +367,8 @@ export async function DELETE(
     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,
diff --git a/apps/sim/app/api/credentials/route.ts b/apps/sim/app/api/credentials/route.ts
index 3d8877cc49e..0b210325064 100644
--- a/apps/sim/app/api/credentials/route.ts
+++ b/apps/sim/app/api/credentials/route.ts
@@ -616,6 +616,8 @@ 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,
diff --git a/apps/sim/app/api/schedules/[id]/route.ts b/apps/sim/app/api/schedules/[id]/route.ts
index 3236bba1047..d05514a8837 100644
--- a/apps/sim/app/api/schedules/[id]/route.ts
+++ b/apps/sim/app/api/schedules/[id]/route.ts
@@ -146,6 +146,8 @@ 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,
@@ -209,6 +211,8 @@ 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,
@@ -253,6 +257,8 @@ 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,
@@ -300,6 +306,8 @@ export async function DELETE(
     recordAudit({
       workspaceId,
       actorId: session.user.id,
+      actorName: session.user.name,
+      actorEmail: session.user.email,
       action: AuditAction.SCHEDULE_DELETED,
       resourceType: AuditResourceType.SCHEDULE,
       resourceId: scheduleId,
diff --git a/apps/sim/app/api/schedules/route.ts b/apps/sim/app/api/schedules/route.ts
index b30b704882f..a2f14f109a8 100644
--- a/apps/sim/app/api/schedules/route.ts
+++ b/apps/sim/app/api/schedules/route.ts
@@ -283,6 +283,8 @@ export async function POST(req: NextRequest) {
     recordAudit({
       workspaceId,
       actorId: session.user.id,
+      actorName: session.user.name,
+      actorEmail: session.user.email,
       action: AuditAction.SCHEDULE_CREATED,
       resourceType: AuditResourceType.SCHEDULE,
       resourceId: id,

From e0ab84465a73044bfd936b030c675a6d11b54a64 Mon Sep 17 00:00:00 2001
From: Waleed Latif 
Date: Sat, 11 Apr 2026 12:20:24 -0700
Subject: [PATCH 10/17] fix(testing): sync audit mock with new AuditAction and
 AuditResourceType entries

Co-Authored-By: Claude Opus 4.6 
---
 packages/testing/src/mocks/audit.mock.ts | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts
index 0b4a46d9deb..3665c5ce05b 100644
--- a/packages/testing/src/mocks/audit.mock.ts
+++ b/packages/testing/src/mocks/audit.mock.ts
@@ -22,6 +22,8 @@ export const auditMock = {
     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',
@@ -75,6 +77,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',
@@ -90,7 +93,9 @@ export const auditMock = {
     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',
@@ -124,6 +129,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 6c2495baa4b7fa4cf7546d958e917f69ba61cfc0 Mon Sep 17 00:00:00 2001
From: Waleed Latif 
Date: Sat, 11 Apr 2026 12:25:26 -0700
Subject: [PATCH 11/17] 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 
---
 .../ee/audit-logs/components/audit-logs.tsx   | 45 ++++++++-----------
 1 file changed, 18 insertions(+), 27 deletions(-)

diff --git a/apps/sim/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx
index 6cf59d98f26..463f4e8e738 100644
--- a/apps/sim/ee/audit-logs/components/audit-logs.tsx
+++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx
@@ -7,40 +7,31 @@ import { Badge, Button, Combobox, type ComboboxOption, Skeleton } from '@/compon
 import { Input } from '@/components/ui'
 import { cn } from '@/lib/core/utils/cn'
 import { formatDateTime } from '@/lib/core/utils/formatting'
+import { AuditResourceType } from '@/lib/audit/log'
 import type { EnterpriseAuditLogEntry } from '@/app/api/v1/audit-logs/format'
 import { type AuditLogFilters, useAuditLogs } from '@/ee/audit-logs/hooks/audit-logs'
 
 const logger = createLogger('AuditLogs')
 
+const ACRONYMS = new Set(['API', 'BYOK', 'MCP', 'OAuth'])
+
+function formatResourceLabel(key: string): string {
+  const words = key.split('_')
+  return words
+    .map((w) => {
+      const upper = w.toUpperCase()
+      if (ACRONYMS.has(upper)) return upper
+      return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
+    })
+    .join(' ')
+    .replace('OAUTH', 'OAuth')
+}
+
 const RESOURCE_TYPE_OPTIONS: ComboboxOption[] = [
   { label: 'All Types', value: '' },
-  { label: 'API Key', value: 'api_key' },
-  { label: 'Billing', value: 'billing' },
-  { label: 'BYOK Key', value: 'byok_key' },
-  { label: 'Chat', value: 'chat' },
-  { label: 'Connector', value: 'connector' },
-  { label: 'Credential', value: 'credential' },
-  { label: 'Credential Set', value: 'credential_set' },
-  { label: 'Custom Tool', value: 'custom_tool' },
-  { label: 'Document', value: 'document' },
-  { label: 'Environment', value: 'environment' },
-  { label: 'File', value: 'file' },
-  { label: 'Folder', value: 'folder' },
-  { label: 'Form', value: 'form' },
-  { label: 'Knowledge Base', value: 'knowledge_base' },
-  { label: 'MCP Server', value: 'mcp_server' },
-  { label: 'Notification', value: 'notification' },
-  { label: 'OAuth', value: 'oauth' },
-  { label: 'Organization', value: 'organization' },
-  { label: 'Password', value: 'password' },
-  { label: 'Permission Group', value: 'permission_group' },
-  { label: 'Schedule', value: 'schedule' },
-  { label: 'Skill', value: 'skill' },
-  { label: 'Table', value: 'table' },
-  { label: 'Template', value: 'template' },
-  { label: 'Webhook', value: 'webhook' },
-  { label: 'Workflow', value: 'workflow' },
-  { label: 'Workspace', value: 'workspace' },
+  ...(Object.entries(AuditResourceType) as [string, string][])
+    .map(([key, value]) => ({ label: formatResourceLabel(key), value }))
+    .sort((a, b) => a.label.localeCompare(b.label)),
 ]
 
 const DATE_RANGE_OPTIONS: ComboboxOption[] = [

From 4021cb259866e5f3e76be770d0530b1720c0a01b Mon Sep 17 00:00:00 2001
From: Waleed Latif 
Date: Sat, 11 Apr 2026 12:39:51 -0700
Subject: [PATCH 12/17] 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 
---
 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 |  8 ++++-
 .../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/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 |  4 +++
 .../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/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/audit-logs/query.ts       |  6 ++--
 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     |  5 +++
 .../app/api/workflows/[id]/duplicate/route.ts |  8 ++++-
 .../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 +-
 .../ee/audit-logs/components/audit-logs.tsx   | 23 +------------
 apps/sim/ee/audit-logs/constants.ts           | 23 +++++++++++++
 apps/sim/lib/audit/log.ts                     |  5 +++
 apps/sim/lib/auth/auth.ts                     |  1 +
 .../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    | 20 +++++++++++-
 .../sim/lib/workflows/orchestration/deploy.ts | 19 +++++++++--
 .../orchestration/workflow-lifecycle.ts       |  2 ++
 packages/testing/src/mocks/audit.mock.ts      |  5 +++
 82 files changed, 605 insertions(+), 83 deletions(-)
 create mode 100644 apps/sim/ee/audit-logs/constants.ts

diff --git a/apps/sim/app/api/billing/credits/route.ts b/apps/sim/app/api/billing/credits/route.ts
index 7dfeafb2efe..070b3893133 100644
--- a/apps/sim/app/api/billing/credits/route.ts
+++ b/apps/sim/app/api/billing/credits/route.ts
@@ -64,8 +64,12 @@ export async function POST(request: NextRequest) {
       actorEmail: session.user.email,
       action: AuditAction.CREDIT_PURCHASED,
       resourceType: AuditResourceType.BILLING,
+      resourceId: validation.data.requestId,
       description: `Purchased $${validation.data.amount} in credits`,
-      metadata: { amount: validation.data.amount, requestId: validation.data.requestId },
+      metadata: {
+        amountDollars: validation.data.amount,
+        requestId: validation.data.requestId,
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/chat/manage/[id]/route.ts b/apps/sim/app/api/chat/manage/[id]/route.ts
index c09688c99d6..8cf37410ae0 100644
--- a/apps/sim/app/api/chat/manage/[id]/route.ts
+++ b/apps/sim/app/api/chat/manage/[id]/route.ts
@@ -233,6 +233,12 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
         resourceId: chatId,
         resourceName: title || existingChatRecord.title,
         description: `Updated chat deployment "${title || existingChatRecord.title}"`,
+        metadata: {
+          identifier: updatedIdentifier,
+          authType: updateData.authType || existingChatRecord.authType,
+          workflowId: workflowId || existingChatRecord.workflowId,
+          chatUrl,
+        },
         request,
       })
 
diff --git a/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts b/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts
index 9a91b86b8e2..752ebc1a9e7 100644
--- a/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts
+++ b/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts
@@ -159,7 +159,12 @@ export async function POST(
       resourceId: id,
       resourceName: result.set.name,
       description: `Resent credential set invitation to ${invitation.email}`,
-      metadata: { invitationId, targetEmail: invitation.email },
+      metadata: {
+        invitationId,
+        targetEmail: invitation.email,
+        providerId: result.set.providerId,
+        credentialSetName: result.set.name,
+      },
       request: req,
     })
 
diff --git a/apps/sim/app/api/credential-sets/[id]/invite/route.ts b/apps/sim/app/api/credential-sets/[id]/invite/route.ts
index cd5ebb53015..b9b0ccc4a95 100644
--- a/apps/sim/app/api/credential-sets/[id]/invite/route.ts
+++ b/apps/sim/app/api/credential-sets/[id]/invite/route.ts
@@ -187,7 +187,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
       actorEmail: session.user.email ?? undefined,
       resourceName: result.set.name,
       description: `Created invitation for credential set "${result.set.name}"${email ? ` to ${email}` : ''}`,
-      metadata: { targetEmail: email || undefined },
+      metadata: {
+        invitationId: invitation.id,
+        targetEmail: email || undefined,
+        providerId: result.set.providerId,
+        credentialSetName: result.set.name,
+      },
       request: req,
     })
 
diff --git a/apps/sim/app/api/credential-sets/[id]/members/route.ts b/apps/sim/app/api/credential-sets/[id]/members/route.ts
index e6ffbaa6262..aca1b63d648 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,13 @@ 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,
+        credentialSetName: result.set.name,
+      },
       request: req,
     })
 
diff --git a/apps/sim/app/api/credential-sets/[id]/route.ts b/apps/sim/app/api/credential-sets/[id]/route.ts
index 51110916e93..d522cf9c3df 100644
--- a/apps/sim/app/api/credential-sets/[id]/route.ts
+++ b/apps/sim/app/api/credential-sets/[id]/route.ts
@@ -142,6 +142,13 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
       actorEmail: session.user.email ?? undefined,
       resourceName: updated?.name ?? result.set.name,
       description: `Updated credential set "${updated?.name ?? result.set.name}"`,
+      metadata: {
+        organizationId: result.set.organizationId,
+        providerId: result.set.providerId,
+        updatedFields: Object.keys(updates).filter(
+          (k) => updates[k as keyof typeof updates] !== undefined
+        ),
+      },
       request: req,
     })
 
@@ -199,6 +206,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
       actorEmail: session.user.email ?? undefined,
       resourceName: result.set.name,
       description: `Deleted credential set "${result.set.name}"`,
+      metadata: { organizationId: result.set.organizationId, providerId: result.set.providerId },
       request: req,
     })
 
diff --git a/apps/sim/app/api/credential-sets/invite/[token]/route.ts b/apps/sim/app/api/credential-sets/invite/[token]/route.ts
index 656d39fdde1..fc3759b0e27 100644
--- a/apps/sim/app/api/credential-sets/invite/[token]/route.ts
+++ b/apps/sim/app/api/credential-sets/invite/[token]/route.ts
@@ -192,7 +192,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
       resourceId: invitation.credentialSetId,
       resourceName: invitation.credentialSetName,
       description: `Accepted credential set invitation`,
-      metadata: { invitationId: invitation.id },
+      metadata: {
+        invitationId: invitation.id,
+        credentialSetId: invitation.credentialSetId,
+        providerId: invitation.providerId,
+        credentialSetName: invitation.credentialSetName,
+      },
       request: req,
     })
 
diff --git a/apps/sim/app/api/credential-sets/memberships/route.ts b/apps/sim/app/api/credential-sets/memberships/route.ts
index aef704f7b9c..926714b98f9 100644
--- a/apps/sim/app/api/credential-sets/memberships/route.ts
+++ b/apps/sim/app/api/credential-sets/memberships/route.ts
@@ -116,6 +116,7 @@ export async function DELETE(req: NextRequest) {
       resourceType: AuditResourceType.CREDENTIAL_SET,
       resourceId: credentialSetId,
       description: `Left credential set`,
+      metadata: { credentialSetId },
       request: req,
     })
 
diff --git a/apps/sim/app/api/credential-sets/route.ts b/apps/sim/app/api/credential-sets/route.ts
index b5166630af9..c120e84b421 100644
--- a/apps/sim/app/api/credential-sets/route.ts
+++ b/apps/sim/app/api/credential-sets/route.ts
@@ -179,6 +179,7 @@ export async function POST(req: Request) {
       actorEmail: session.user.email ?? undefined,
       resourceName: name,
       description: `Created credential set "${name}"`,
+      metadata: { organizationId, providerId, credentialSetName: name },
       request: req,
     })
 
diff --git a/apps/sim/app/api/environment/route.ts b/apps/sim/app/api/environment/route.ts
index 229ba26382f..f8167e92ac2 100644
--- a/apps/sim/app/api/environment/route.ts
+++ b/apps/sim/app/api/environment/route.ts
@@ -67,8 +67,13 @@ export async function POST(req: NextRequest) {
         actorEmail: session.user.email,
         action: AuditAction.ENVIRONMENT_UPDATED,
         resourceType: AuditResourceType.ENVIRONMENT,
-        description: 'Updated global environment variables',
-        metadata: { variableCount: Object.keys(variables).length },
+        resourceId: session.user.id,
+        description: `Updated ${Object.keys(variables).length} personal environment variable(s)`,
+        metadata: {
+          variableCount: Object.keys(variables).length,
+          updatedKeys: Object.keys(variables),
+          scope: 'personal',
+        },
         request: req,
       })
 
diff --git a/apps/sim/app/api/folders/route.ts b/apps/sim/app/api/folders/route.ts
index 98e80f5aa3d..37e0ae8d1d8 100644
--- a/apps/sim/app/api/folders/route.ts
+++ b/apps/sim/app/api/folders/route.ts
@@ -168,7 +168,13 @@ export async function POST(request: NextRequest) {
       resourceId: id,
       resourceName: name.trim(),
       description: `Created folder "${name.trim()}"`,
-      metadata: { name: name.trim() },
+      metadata: {
+        name: name.trim(),
+        workspaceId,
+        parentId: parentId || undefined,
+        color: color || '#6B7280',
+        sortOrder: newFolder.sortOrder,
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/form/manage/[id]/route.ts b/apps/sim/app/api/form/manage/[id]/route.ts
index 577363b8d9c..a57a7c937bb 100644
--- a/apps/sim/app/api/form/manage/[id]/route.ts
+++ b/apps/sim/app/api/form/manage/[id]/route.ts
@@ -197,8 +197,14 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
         resourceId: id,
         actorName: session.user.name ?? undefined,
         actorEmail: session.user.email ?? undefined,
-        resourceName: formRecord.title ?? undefined,
-        description: `Updated form "${formRecord.title}"`,
+        resourceName: (title || formRecord.title) ?? undefined,
+        description: `Updated form "${title || formRecord.title}"`,
+        metadata: {
+          identifier: identifier || formRecord.identifier,
+          workflowId: formRecord.workflowId,
+          authType: authType || formRecord.authType,
+          updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'),
+        },
         request,
       })
 
@@ -255,6 +261,7 @@ export async function DELETE(
       actorEmail: session.user.email ?? undefined,
       resourceName: formRecord.title ?? undefined,
       description: `Deleted form "${formRecord.title}"`,
+      metadata: { identifier: formRecord.identifier, workflowId: formRecord.workflowId },
       request,
     })
 
diff --git a/apps/sim/app/api/form/route.ts b/apps/sim/app/api/form/route.ts
index 6512ba95808..db29c2759de 100644
--- a/apps/sim/app/api/form/route.ts
+++ b/apps/sim/app/api/form/route.ts
@@ -208,6 +208,7 @@ export async function POST(request: NextRequest) {
         actorEmail: session.user.email ?? undefined,
         resourceName: title,
         description: `Created form "${title}" for workflow ${workflowId}`,
+        metadata: { identifier, workflowId, authType, formUrl, showBranding },
         request,
       })
 
diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts
index 48e0d0deb2d..c5e7878fc69 100644
--- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts
@@ -194,7 +194,13 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
         resourceType: AuditResourceType.CONNECTOR,
         resourceId: connectorId,
         description: `Restored ${updated.length} excluded document(s) for knowledge base "${writeCheck.knowledgeBase.name}"`,
-        metadata: { knowledgeBaseId, documentCount: updated.length },
+        metadata: {
+          knowledgeBaseId,
+          knowledgeBaseName: writeCheck.knowledgeBase.name,
+          operation: 'restore',
+          documentCount: updated.length,
+          documentIds: updated.map((d) => d.id),
+        },
         request,
       })
 
@@ -229,7 +235,13 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
       resourceType: AuditResourceType.CONNECTOR,
       resourceId: connectorId,
       description: `Excluded ${updated.length} document(s) from knowledge base "${writeCheck.knowledgeBase.name}"`,
-      metadata: { knowledgeBaseId, documentCount: updated.length },
+      metadata: {
+        knowledgeBaseId,
+        knowledgeBaseName: writeCheck.knowledgeBase.name,
+        operation: 'exclude',
+        documentCount: updated.length,
+        documentIds: updated.map((d) => d.id),
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts
index 87cdb51a737..6ffee2355a1 100644
--- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts
@@ -268,7 +268,16 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
       resourceId: connectorId,
       resourceName: updatedData.connectorType,
       description: `Updated connector for knowledge base "${writeCheck.knowledgeBase.name}"`,
-      metadata: { knowledgeBaseId, updatedFields: Object.keys(parsed.data) },
+      metadata: {
+        knowledgeBaseId,
+        knowledgeBaseName: writeCheck.knowledgeBase.name,
+        connectorType: updatedData.connectorType,
+        updatedFields: Object.keys(parsed.data),
+        ...(parsed.data.syncIntervalMinutes !== undefined && {
+          syncIntervalMinutes: parsed.data.syncIntervalMinutes,
+        }),
+        ...(parsed.data.status !== undefined && { newStatus: parsed.data.status }),
+      },
       request,
     })
 
@@ -399,6 +408,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
       description: `Deleted connector from knowledge base "${writeCheck.knowledgeBase.name}"`,
       metadata: {
         knowledgeBaseId,
+        knowledgeBaseName: writeCheck.knowledgeBase.name,
+        connectorType: existingConnector[0].connectorType,
+        deleteDocuments,
         documentsDeleted: deleteDocuments ? docCount : 0,
         documentsKept: deleteDocuments ? 0 : docCount,
       },
diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts
index df7057fc904..1ace24c886b 100644
--- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts
@@ -78,7 +78,13 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
       resourceId: connectorId,
       resourceName: connectorRows[0].connectorType,
       description: `Triggered manual sync for connector on knowledge base "${writeCheck.knowledgeBase.name}"`,
-      metadata: { knowledgeBaseId },
+      metadata: {
+        knowledgeBaseId,
+        knowledgeBaseName: writeCheck.knowledgeBase.name,
+        connectorType: connectorRows[0].connectorType,
+        connectorStatus: connectorRows[0].status,
+        syncType: 'manual',
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/knowledge/[id]/connectors/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/route.ts
index b5e2cb86f46..34da8e03276 100644
--- a/apps/sim/app/api/knowledge/[id]/connectors/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/connectors/route.ts
@@ -286,7 +286,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
       resourceId: connectorId,
       resourceName: connectorType,
       description: `Created ${connectorType} connector for knowledge base "${writeCheck.knowledgeBase.name}"`,
-      metadata: { knowledgeBaseId, connectorType, syncIntervalMinutes },
+      metadata: {
+        knowledgeBaseId,
+        knowledgeBaseName: writeCheck.knowledgeBase.name,
+        connectorType,
+        syncIntervalMinutes,
+        authMode: connectorConfig.auth.mode,
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts
index 4f8735826b1..f238ac4f978 100644
--- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts
@@ -208,7 +208,16 @@ export async function PUT(
           resourceType: AuditResourceType.DOCUMENT,
           resourceId: documentId,
           resourceName: validatedData.filename ?? accessCheck.document?.filename,
-          description: `Updated document "${documentId}" in knowledge base "${knowledgeBaseId}"`,
+          description: `Updated document "${validatedData.filename ?? accessCheck.document?.filename}" in knowledge base "${knowledgeBaseId}"`,
+          metadata: {
+            knowledgeBaseId,
+            knowledgeBaseName: accessCheck.knowledgeBase?.name,
+            fileName: validatedData.filename ?? accessCheck.document?.filename,
+            updatedFields: Object.keys(validatedData).filter(
+              (k) => validatedData[k as keyof typeof validatedData] !== undefined
+            ),
+            ...(validatedData.enabled !== undefined && { enabled: validatedData.enabled }),
+          },
           request: req,
         })
 
@@ -281,8 +290,14 @@ export async function DELETE(
       resourceType: AuditResourceType.DOCUMENT,
       resourceId: documentId,
       resourceName: accessCheck.document?.filename,
-      description: `Deleted document "${documentId}" from knowledge base "${knowledgeBaseId}"`,
-      metadata: { fileName: accessCheck.document?.filename },
+      description: `Deleted document "${accessCheck.document?.filename}" from knowledge base "${knowledgeBaseId}"`,
+      metadata: {
+        knowledgeBaseId,
+        knowledgeBaseName: accessCheck.knowledgeBase?.name,
+        fileName: accessCheck.document?.filename,
+        fileSize: accessCheck.document?.fileSize,
+        mimeType: accessCheck.document?.mimeType,
+      },
       request: req,
     })
 
diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/documents/route.ts
index 83056e8f486..24ef30e603b 100644
--- a/apps/sim/app/api/knowledge/[id]/documents/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts
@@ -278,8 +278,10 @@ 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),
+            uploadType: 'bulk',
           },
           request: req,
         })
@@ -358,9 +360,11 @@ 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,
+            uploadType: 'single',
           },
           request: req,
         })
diff --git a/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts b/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts
index 59be57cd610..8d5ee153918 100644
--- a/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts
@@ -196,7 +196,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
         ? `Upserted (replaced) document "${validatedData.filename}" in knowledge base "${knowledgeBaseId}"`
         : `Upserted (created) document "${validatedData.filename}" in knowledge base "${knowledgeBaseId}"`,
       metadata: {
+        knowledgeBaseName: accessCheck.knowledgeBase?.name,
         fileName: validatedData.filename,
+        fileType: validatedData.mimeType,
+        fileSize: validatedData.fileSize,
         previousDocumentId: existingDocumentId,
         isUpdate,
       },
diff --git a/apps/sim/app/api/knowledge/[id]/restore/route.ts b/apps/sim/app/api/knowledge/[id]/restore/route.ts
index 1d37f664ab7..02d8b3e5afd 100644
--- a/apps/sim/app/api/knowledge/[id]/restore/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/restore/route.ts
@@ -59,6 +59,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
       resourceId: id,
       resourceName: kb.name,
       description: `Restored knowledge base "${kb.name}"`,
+      metadata: {
+        knowledgeBaseName: kb.name,
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/knowledge/[id]/route.ts b/apps/sim/app/api/knowledge/[id]/route.ts
index 2dcf53701da..5da7026a454 100644
--- a/apps/sim/app/api/knowledge/[id]/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/route.ts
@@ -147,6 +147,20 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
         resourceId: id,
         resourceName: validatedData.name ?? updatedKnowledgeBase.name,
         description: `Updated knowledge base "${validatedData.name ?? updatedKnowledgeBase.name}"`,
+        metadata: {
+          updatedFields: Object.keys(validatedData).filter(
+            (k) => validatedData[k as keyof typeof validatedData] !== undefined
+          ),
+          ...(validatedData.name && { newName: validatedData.name }),
+          ...(validatedData.description !== undefined && {
+            description: validatedData.description,
+          }),
+          ...(validatedData.chunkingConfig && {
+            chunkMaxSize: validatedData.chunkingConfig.maxSize,
+            chunkMinSize: validatedData.chunkingConfig.minSize,
+            chunkOverlap: validatedData.chunkingConfig.overlap,
+          }),
+        },
         request: req,
       })
 
@@ -226,6 +240,9 @@ export async function DELETE(
       resourceId: id,
       resourceName: accessCheck.knowledgeBase.name,
       description: `Deleted knowledge base "${accessCheck.knowledgeBase.name || id}"`,
+      metadata: {
+        knowledgeBaseName: accessCheck.knowledgeBase.name,
+      },
       request: _request,
     })
 
diff --git a/apps/sim/app/api/knowledge/route.ts b/apps/sim/app/api/knowledge/route.ts
index 20499ce8fce..9641f3a4539 100644
--- a/apps/sim/app/api/knowledge/route.ts
+++ b/apps/sim/app/api/knowledge/route.ts
@@ -162,7 +162,16 @@ export async function POST(req: NextRequest) {
         resourceId: newKnowledgeBase.id,
         resourceName: validatedData.name,
         description: `Created knowledge base "${validatedData.name}"`,
-        metadata: { name: validatedData.name },
+        metadata: {
+          name: validatedData.name,
+          description: validatedData.description,
+          embeddingModel: validatedData.embeddingModel,
+          embeddingDimension: validatedData.embeddingDimension,
+          chunkingStrategy: validatedData.chunkingConfig.strategy,
+          chunkMaxSize: validatedData.chunkingConfig.maxSize,
+          chunkMinSize: validatedData.chunkingConfig.minSize,
+          chunkOverlap: validatedData.chunkingConfig.overlap,
+        },
         request: req,
       })
 
diff --git a/apps/sim/app/api/mcp/servers/[id]/route.ts b/apps/sim/app/api/mcp/servers/[id]/route.ts
index 54265bb687c..67c893fc754 100644
--- a/apps/sim/app/api/mcp/servers/[id]/route.ts
+++ b/apps/sim/app/api/mcp/servers/[id]/route.ts
@@ -124,6 +124,14 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
         resourceId: serverId,
         resourceName: updatedServer.name || serverId,
         description: `Updated MCP server "${updatedServer.name || serverId}"`,
+        metadata: {
+          serverName: updatedServer.name,
+          transport: updatedServer.transport,
+          url: updatedServer.url,
+          updatedFields: Object.keys(updateData).filter(
+            (k) => k !== 'workspaceId' && k !== 'updatedAt'
+          ),
+        },
         request,
       })
 
diff --git a/apps/sim/app/api/mcp/servers/route.ts b/apps/sim/app/api/mcp/servers/route.ts
index 054c7a3a2ca..5d5c1d8b6fe 100644
--- a/apps/sim/app/api/mcp/servers/route.ts
+++ b/apps/sim/app/api/mcp/servers/route.ts
@@ -206,7 +206,14 @@ export const POST = withMcpAuth('write')(
         resourceId: serverId,
         resourceName: body.name,
         description: `Added MCP server "${body.name}"`,
-        metadata: { serverName: body.name, transport: body.transport },
+        metadata: {
+          serverName: body.name,
+          transport: body.transport,
+          url: body.url,
+          timeout: body.timeout || 30000,
+          retries: body.retries || 3,
+          source: source,
+        },
         request,
       })
 
@@ -278,6 +285,12 @@ export const DELETE = withMcpAuth('admin')(
         resourceId: serverId!,
         resourceName: deletedServer.name,
         description: `Removed MCP server "${deletedServer.name}"`,
+        metadata: {
+          serverName: deletedServer.name,
+          transport: deletedServer.transport,
+          url: deletedServer.url,
+          source,
+        },
         request,
       })
 
diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts
index f5ed5371e19..6ed1bb2e0c6 100644
--- a/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts
+++ b/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts
@@ -135,6 +135,11 @@ export const PATCH = withMcpAuth('write')(
         resourceId: serverId,
         resourceName: updatedServer.name,
         description: `Updated workflow MCP server "${updatedServer.name}"`,
+        metadata: {
+          serverName: updatedServer.name,
+          isPublic: updatedServer.isPublic,
+          updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'),
+        },
         request,
       })
 
@@ -189,6 +194,7 @@ export const DELETE = withMcpAuth('admin')(
         resourceId: serverId,
         resourceName: deletedServer.name,
         description: `Unpublished workflow MCP server "${deletedServer.name}"`,
+        metadata: { serverName: deletedServer.name },
         request,
       })
 
diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts
index f54caf4703e..60791a36bcf 100644
--- a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts
+++ b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts
@@ -152,7 +152,12 @@ export const PATCH = withMcpAuth('write')(
         resourceType: AuditResourceType.MCP_SERVER,
         resourceId: serverId,
         description: `Updated tool "${updatedTool.toolName}" in MCP server`,
-        metadata: { toolId, toolName: updatedTool.toolName },
+        metadata: {
+          toolId,
+          toolName: updatedTool.toolName,
+          workflowId: updatedTool.workflowId,
+          updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'),
+        },
         request,
       })
 
@@ -220,7 +225,7 @@ export const DELETE = withMcpAuth('write')(
         resourceType: AuditResourceType.MCP_SERVER,
         resourceId: serverId,
         description: `Removed tool "${deletedTool.toolName}" from MCP server`,
-        metadata: { toolId, toolName: deletedTool.toolName },
+        metadata: { toolId, toolName: deletedTool.toolName, workflowId: deletedTool.workflowId },
         request,
       })
 
diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts
index 1a4687b44fc..396cfe92468 100644
--- a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts
+++ b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts
@@ -224,7 +224,13 @@ export const POST = withMcpAuth('write')(
         resourceType: AuditResourceType.MCP_SERVER,
         resourceId: serverId,
         description: `Added tool "${toolName}" to MCP server`,
-        metadata: { toolId, toolName, workflowId: body.workflowId },
+        metadata: {
+          toolId,
+          toolName,
+          toolDescription,
+          workflowId: body.workflowId,
+          workflowName: workflowRecord.name,
+        },
         request,
       })
 
diff --git a/apps/sim/app/api/mcp/workflow-servers/route.ts b/apps/sim/app/api/mcp/workflow-servers/route.ts
index 84d431fa423..807df769673 100644
--- a/apps/sim/app/api/mcp/workflow-servers/route.ts
+++ b/apps/sim/app/api/mcp/workflow-servers/route.ts
@@ -208,6 +208,13 @@ export const POST = withMcpAuth('write')(
         resourceId: serverId,
         resourceName: body.name.trim(),
         description: `Published workflow MCP server "${body.name.trim()}" with ${addedTools.length} tool(s)`,
+        metadata: {
+          serverName: body.name.trim(),
+          isPublic: body.isPublic ?? false,
+          toolCount: addedTools.length,
+          toolNames: addedTools.map((t) => t.toolName),
+          workflowIds: addedTools.map((t) => t.workflowId),
+        },
         request,
       })
 
diff --git a/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts b/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts
index f54e72b2701..7f4f7d8004c 100644
--- a/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts
+++ b/apps/sim/app/api/organizations/[id]/invitations/[invitationId]/route.ts
@@ -182,6 +182,20 @@ export async function POST(
       email: orgInvitation.email,
     })
 
+    recordAudit({
+      workspaceId: null,
+      actorId: session.user.id,
+      action: AuditAction.ORG_INVITATION_RESENT,
+      resourceType: AuditResourceType.ORGANIZATION,
+      resourceId: organizationId,
+      actorName: session.user.name ?? undefined,
+      actorEmail: session.user.email ?? undefined,
+      resourceName: org?.name ?? undefined,
+      description: `Resent organization invitation to ${orgInvitation.email}`,
+      metadata: { invitationId, targetEmail: orgInvitation.email, targetRole: orgInvitation.role },
+      request: _request,
+    })
+
     return NextResponse.json({
       success: true,
       message: 'Invitation resent successfully',
diff --git a/apps/sim/app/api/organizations/[id]/invitations/route.ts b/apps/sim/app/api/organizations/[id]/invitations/route.ts
index 001184d98e7..5a85cfbb4f0 100644
--- a/apps/sim/app/api/organizations/[id]/invitations/route.ts
+++ b/apps/sim/app/api/organizations/[id]/invitations/route.ts
@@ -423,7 +423,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
         actorEmail: session.user.email ?? undefined,
         resourceName: organizationEntry[0]?.name,
         description: `Invited ${inv.email} to organization as ${role}`,
-        metadata: { invitationId: inv.id, targetEmail: inv.email, targetRole: role },
+        metadata: {
+          invitationId: inv.id,
+          targetEmail: inv.email,
+          targetRole: role,
+          isBatch,
+          workspaceInvitationCount: validWorkspaceInvitations.length,
+        },
         request,
       })
     }
@@ -558,7 +564,7 @@ export async function DELETE(
       actorName: session.user.name ?? undefined,
       actorEmail: session.user.email ?? undefined,
       description: `Revoked organization invitation for ${result[0].email}`,
-      metadata: { invitationId, targetEmail: result[0].email },
+      metadata: { invitationId, targetEmail: result[0].email, targetRole: result[0].role },
       request,
     })
 
diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts
index 3b15d34848e..989d792b6fd 100644
--- a/apps/sim/app/api/organizations/[id]/members/route.ts
+++ b/apps/sim/app/api/organizations/[id]/members/route.ts
@@ -294,6 +294,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
       resourceId: organizationId,
       actorName: session.user.name ?? undefined,
       actorEmail: session.user.email ?? undefined,
+      resourceName: organizationEntry[0]?.name ?? undefined,
       description: `Invited ${normalizedEmail} to organization as ${role}`,
       metadata: { invitationId, targetEmail: normalizedEmail, targetRole: role },
       request,
diff --git a/apps/sim/app/api/organizations/route.ts b/apps/sim/app/api/organizations/route.ts
index 5803f85dc25..6185f120f46 100644
--- a/apps/sim/app/api/organizations/route.ts
+++ b/apps/sim/app/api/organizations/route.ts
@@ -126,6 +126,7 @@ export async function POST(request: Request) {
       actorEmail: user.email ?? undefined,
       resourceName: organizationName ?? undefined,
       description: `Created organization "${organizationName}"`,
+      metadata: { organizationSlug },
       request,
     })
 
diff --git a/apps/sim/app/api/permission-groups/[id]/route.ts b/apps/sim/app/api/permission-groups/[id]/route.ts
index 9391b67d826..51cbe1222b6 100644
--- a/apps/sim/app/api/permission-groups/[id]/route.ts
+++ b/apps/sim/app/api/permission-groups/[id]/route.ts
@@ -193,6 +193,12 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
       actorEmail: session.user.email ?? undefined,
       resourceName: updated.name,
       description: `Updated permission group "${updated.name}"`,
+      metadata: {
+        organizationId: result.group.organizationId,
+        updatedFields: Object.keys(updates).filter(
+          (k) => updates[k as keyof typeof updates] !== undefined
+        ),
+      },
       request: req,
     })
 
@@ -254,6 +260,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
       actorEmail: session.user.email ?? undefined,
       resourceName: result.group.name,
       description: `Deleted permission group "${result.group.name}"`,
+      metadata: { organizationId: result.group.organizationId },
       request: req,
     })
 
diff --git a/apps/sim/app/api/permission-groups/route.ts b/apps/sim/app/api/permission-groups/route.ts
index b79f01ebc5f..9b88d482617 100644
--- a/apps/sim/app/api/permission-groups/route.ts
+++ b/apps/sim/app/api/permission-groups/route.ts
@@ -211,6 +211,7 @@ export async function POST(req: Request) {
       actorEmail: session.user.email ?? undefined,
       resourceName: name,
       description: `Created permission group "${name}"`,
+      metadata: { organizationId, autoAddNewMembers: autoAddNewMembers || false },
       request: req,
     })
 
diff --git a/apps/sim/app/api/skills/route.ts b/apps/sim/app/api/skills/route.ts
index 41173c13188..f1db74a3cce 100644
--- a/apps/sim/app/api/skills/route.ts
+++ b/apps/sim/app/api/skills/route.ts
@@ -103,11 +103,14 @@ export async function POST(req: NextRequest) {
         recordAudit({
           workspaceId,
           actorId: userId,
+          actorName: authResult.userName ?? undefined,
+          actorEmail: authResult.userEmail ?? undefined,
           action: AuditAction.SKILL_CREATED,
           resourceType: AuditResourceType.SKILL,
           resourceId: skill.id,
           resourceName: skill.name,
           description: `Created/updated skill "${skill.name}"`,
+          metadata: { source },
         })
         captureServerEvent(
           userId,
@@ -185,10 +188,13 @@ export async function DELETE(request: NextRequest) {
     recordAudit({
       workspaceId,
       actorId: authResult.userId,
+      actorName: authResult.userName ?? undefined,
+      actorEmail: authResult.userEmail ?? undefined,
       action: AuditAction.SKILL_DELETED,
       resourceType: AuditResourceType.SKILL,
       resourceId: skillId,
       description: `Deleted skill`,
+      metadata: { source },
     })
 
     captureServerEvent(
diff --git a/apps/sim/app/api/table/[tableId]/restore/route.ts b/apps/sim/app/api/table/[tableId]/restore/route.ts
index 9175de0b661..fca864c8753 100644
--- a/apps/sim/app/api/table/[tableId]/restore/route.ts
+++ b/apps/sim/app/api/table/[tableId]/restore/route.ts
@@ -45,6 +45,10 @@ export async function POST(
       resourceId: tableId,
       resourceName: table.name,
       description: `Restored table "${table.name}"`,
+      metadata: {
+        tableName: table.name,
+        workspaceId: table.workspaceId,
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/templates/[id]/route.ts b/apps/sim/app/api/templates/[id]/route.ts
index 260b64f582b..82c73fffa0f 100644
--- a/apps/sim/app/api/templates/[id]/route.ts
+++ b/apps/sim/app/api/templates/[id]/route.ts
@@ -251,6 +251,15 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
       resourceId: id,
       resourceName: name ?? template.name,
       description: `Updated template "${name ?? template.name}"`,
+      metadata: {
+        templateName: name ?? template.name,
+        updatedFields: Object.keys(validationResult.data).filter(
+          (k) => validationResult.data[k as keyof typeof validationResult.data] !== undefined
+        ),
+        statusChange: status !== undefined ? { from: template.status, to: status } : undefined,
+        stateUpdated: updateState || false,
+        workflowId: template.workflowId || undefined,
+      },
       request,
     })
 
@@ -317,6 +326,13 @@ export async function DELETE(
       resourceId: id,
       resourceName: template.name,
       description: `Deleted template "${template.name}"`,
+      metadata: {
+        templateName: template.name,
+        workflowId: template.workflowId || undefined,
+        creatorId: template.creatorId || undefined,
+        status: template.status,
+        tags: template.tags,
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/templates/route.ts b/apps/sim/app/api/templates/route.ts
index c6424865c82..0a9f9d02b72 100644
--- a/apps/sim/app/api/templates/route.ts
+++ b/apps/sim/app/api/templates/route.ts
@@ -346,6 +346,14 @@ export async function POST(request: NextRequest) {
       resourceId: templateId,
       resourceName: data.name,
       description: `Created template "${data.name}"`,
+      metadata: {
+        templateName: data.name,
+        workflowId: data.workflowId,
+        creatorId: data.creatorId,
+        tags: data.tags,
+        tagline: data.details?.tagline || undefined,
+        status: 'pending',
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/tools/custom/route.ts b/apps/sim/app/api/tools/custom/route.ts
index 7d45353e609..426da0273ce 100644
--- a/apps/sim/app/api/tools/custom/route.ts
+++ b/apps/sim/app/api/tools/custom/route.ts
@@ -183,11 +183,14 @@ export async function POST(req: NextRequest) {
         recordAudit({
           workspaceId,
           actorId: userId,
+          actorName: authResult.userName ?? undefined,
+          actorEmail: authResult.userEmail ?? undefined,
           action: AuditAction.CUSTOM_TOOL_CREATED,
           resourceType: AuditResourceType.CUSTOM_TOOL,
           resourceId: tool.id,
           resourceName: tool.title,
           description: `Created/updated custom tool "${tool.title}"`,
+          metadata: { source },
         })
       }
 
@@ -304,10 +307,14 @@ export async function DELETE(request: NextRequest) {
     recordAudit({
       workspaceId: tool.workspaceId || undefined,
       actorId: userId,
+      actorName: authResult.userName ?? undefined,
+      actorEmail: authResult.userEmail ?? undefined,
       action: AuditAction.CUSTOM_TOOL_DELETED,
       resourceType: AuditResourceType.CUSTOM_TOOL,
       resourceId: toolId,
-      description: `Deleted custom tool`,
+      resourceName: tool.title,
+      description: `Deleted custom tool "${tool.title}"`,
+      metadata: { source },
     })
 
     logger.info(`[${requestId}] Deleted tool: ${toolId}`)
diff --git a/apps/sim/app/api/v1/audit-logs/query.ts b/apps/sim/app/api/v1/audit-logs/query.ts
index 9fe7c8174cf..db187a218c6 100644
--- a/apps/sim/app/api/v1/audit-logs/query.ts
+++ b/apps/sim/app/api/v1/audit-logs/query.ts
@@ -10,11 +10,11 @@ interface CursorData {
   id: string
 }
 
-export function encodeCursor(data: CursorData): string {
+function encodeCursor(data: CursorData): string {
   return Buffer.from(JSON.stringify(data)).toString('base64')
 }
 
-export function decodeCursor(cursor: string): CursorData | null {
+function decodeCursor(cursor: string): CursorData | null {
   try {
     return JSON.parse(Buffer.from(cursor, 'base64').toString())
   } catch {
@@ -92,7 +92,7 @@ export async function buildOrgScopeCondition(
   return inArray(auditLog.actorId, orgMemberIds)
 }
 
-export function buildCursorCondition(cursor: string): SQL | null {
+function buildCursorCondition(cursor: string): SQL | null {
   const cursorData = decodeCursor(cursor)
   if (!cursorData?.createdAt || !cursorData.id) return null
 
diff --git a/apps/sim/app/api/v1/files/[fileId]/route.ts b/apps/sim/app/api/v1/files/[fileId]/route.ts
index 7007053681b..b3d3db8ceb8 100644
--- a/apps/sim/app/api/v1/files/[fileId]/route.ts
+++ b/apps/sim/app/api/v1/files/[fileId]/route.ts
@@ -142,6 +142,7 @@ export async function DELETE(request: NextRequest, { params }: FileRouteParams)
       resourceId: fileId,
       resourceName: fileRecord.name,
       description: `Archived file "${fileRecord.name}" via API`,
+      metadata: { fileSize: fileRecord.size, fileType: fileRecord.type },
       request,
     })
 
diff --git a/apps/sim/app/api/v1/files/route.ts b/apps/sim/app/api/v1/files/route.ts
index 8c344c1575d..2781fe164a2 100644
--- a/apps/sim/app/api/v1/files/route.ts
+++ b/apps/sim/app/api/v1/files/route.ts
@@ -155,6 +155,7 @@ export async function POST(request: NextRequest) {
       resourceId: userFile.id,
       resourceName: file.name,
       description: `Uploaded file "${file.name}" via API`,
+      metadata: { fileSize: file.size, fileType: file.type || 'application/octet-stream' },
       request,
     })
 
diff --git a/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts b/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts
index b69721329a4..22d40d979f1 100644
--- a/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts
+++ b/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts
@@ -167,6 +167,7 @@ export async function DELETE(request: NextRequest, { params }: DocumentDetailRou
       resourceId: documentId,
       resourceName: docs[0].filename,
       description: `Deleted document "${docs[0].filename}" from knowledge base via API`,
+      metadata: { knowledgeBaseId },
       request,
     })
 
diff --git a/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts b/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts
index 7310a4eca98..6eb61e22614 100644
--- a/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts
+++ b/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts
@@ -207,6 +207,7 @@ export async function POST(request: NextRequest, { params }: DocumentsRouteParam
       resourceId: newDocument.id,
       resourceName: file.name,
       description: `Uploaded document "${file.name}" to knowledge base via API`,
+      metadata: { knowledgeBaseId, fileSize: file.size, mimeType: contentType },
       request,
     })
 
diff --git a/apps/sim/app/api/v1/knowledge/[id]/route.ts b/apps/sim/app/api/v1/knowledge/[id]/route.ts
index 0b7012c8770..b6b5ed3c8ac 100644
--- a/apps/sim/app/api/v1/knowledge/[id]/route.ts
+++ b/apps/sim/app/api/v1/knowledge/[id]/route.ts
@@ -111,6 +111,7 @@ export async function PUT(request: NextRequest, { params }: KnowledgeRouteParams
       resourceId: id,
       resourceName: updatedKb.name,
       description: `Updated knowledge base "${updatedKb.name}" via API`,
+      metadata: { updatedFields: Object.keys(updates) },
       request,
     })
 
diff --git a/apps/sim/app/api/v1/knowledge/route.ts b/apps/sim/app/api/v1/knowledge/route.ts
index 9d45e677bd3..61741d3c59e 100644
--- a/apps/sim/app/api/v1/knowledge/route.ts
+++ b/apps/sim/app/api/v1/knowledge/route.ts
@@ -106,6 +106,7 @@ export async function POST(request: NextRequest) {
       resourceId: kb.id,
       resourceName: kb.name,
       description: `Created knowledge base "${kb.name}" via API`,
+      metadata: { chunkingConfig },
       request,
     })
 
diff --git a/apps/sim/app/api/v1/tables/route.ts b/apps/sim/app/api/v1/tables/route.ts
index 09ff717f9cd..d0c0ad3e64d 100644
--- a/apps/sim/app/api/v1/tables/route.ts
+++ b/apps/sim/app/api/v1/tables/route.ts
@@ -206,6 +206,7 @@ export async function POST(request: NextRequest) {
       resourceId: table.id,
       resourceName: table.name,
       description: `Created table "${table.name}" via API`,
+      metadata: { columnCount: params.schema.columns.length },
       request,
     })
 
diff --git a/apps/sim/app/api/webhooks/[id]/route.ts b/apps/sim/app/api/webhooks/[id]/route.ts
index 24d93fc0609..e146c939507 100644
--- a/apps/sim/app/api/webhooks/[id]/route.ts
+++ b/apps/sim/app/api/webhooks/[id]/route.ts
@@ -270,8 +270,14 @@ export async function DELETE(
       resourceType: AuditResourceType.WEBHOOK,
       resourceId: id,
       resourceName: foundWebhook.provider || 'generic',
-      description: 'Deleted webhook',
-      metadata: { workflowId: webhookData.workflow.id },
+      description: `Deleted ${foundWebhook.provider || 'generic'} webhook`,
+      metadata: {
+        provider: foundWebhook.provider || 'generic',
+        workflowId: webhookData.workflow.id,
+        webhookPath: foundWebhook.path || undefined,
+        blockId: foundWebhook.blockId || undefined,
+        credentialSetId: credentialSetId || undefined,
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/webhooks/route.ts b/apps/sim/app/api/webhooks/route.ts
index c6ef9e992e1..0c7174f4294 100644
--- a/apps/sim/app/api/webhooks/route.ts
+++ b/apps/sim/app/api/webhooks/route.ts
@@ -687,7 +687,12 @@ export async function POST(request: NextRequest) {
         resourceId: savedWebhook.id,
         resourceName: provider || 'generic',
         description: `Created ${provider || 'generic'} webhook`,
-        metadata: { provider, workflowId },
+        metadata: {
+          provider: provider || 'generic',
+          workflowId,
+          webhookPath: finalPath,
+          blockId: blockId || undefined,
+        },
         request,
       })
 
diff --git a/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts b/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts
index a209db29eb4..c5b783b25ec 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,11 @@ export async function POST(
       actorEmail: session!.user.email ?? undefined,
       resourceName: workflowRecord?.name ?? undefined,
       description: `Reverted workflow to deployment version ${version}`,
+      metadata: {
+        targetVersion: version,
+        workflowName: workflowRecord?.name ?? undefined,
+        workspaceId: workflowRecord?.workspaceId ?? undefined,
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/workflows/[id]/duplicate/route.ts b/apps/sim/app/api/workflows/[id]/duplicate/route.ts
index 63c230f686b..b421161a664 100644
--- a/apps/sim/app/api/workflows/[id]/duplicate/route.ts
+++ b/apps/sim/app/api/workflows/[id]/duplicate/route.ts
@@ -87,7 +87,13 @@ 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,
+        newWorkflowName: result.name,
+        workspaceId: workspaceId || undefined,
+        folderId: folderId || undefined,
+      },
       request: req,
     })
 
diff --git a/apps/sim/app/api/workflows/[id]/restore/route.ts b/apps/sim/app/api/workflows/[id]/restore/route.ts
index a9d6b6ba1a5..c0b4d3d535f 100644
--- a/apps/sim/app/api/workflows/[id]/restore/route.ts
+++ b/apps/sim/app/api/workflows/[id]/restore/route.ts
@@ -56,6 +56,10 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
       resourceId: workflowId,
       resourceName: workflowData.name,
       description: `Restored workflow "${workflowData.name}"`,
+      metadata: {
+        workflowName: workflowData.name,
+        workspaceId: workflowData.workspaceId || undefined,
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/workflows/[id]/variables/route.ts b/apps/sim/app/api/workflows/[id]/variables/route.ts
index 1b4cd8ab3b9..064669c9b8f 100644
--- a/apps/sim/app/api/workflows/[id]/variables/route.ts
+++ b/apps/sim/app/api/workflows/[id]/variables/route.ts
@@ -90,7 +90,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
         resourceId: workflowId,
         resourceName: workflowData.name ?? undefined,
         description: `Updated workflow variables`,
-        metadata: { variableCount: Object.keys(variables).length },
+        metadata: {
+          variableCount: Object.keys(variables).length,
+          variableNames: Object.values(variables).map((v) => v.name),
+          workflowName: workflowData.name ?? undefined,
+        },
         request: req,
       })
 
diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts
index 3615afd0890..f96bd6d352f 100644
--- a/apps/sim/app/api/workflows/route.ts
+++ b/apps/sim/app/api/workflows/route.ts
@@ -296,7 +296,14 @@ export async function POST(req: NextRequest) {
       resourceId: workflowId,
       resourceName: name,
       description: `Created workflow "${name}"`,
-      metadata: { name },
+      metadata: {
+        name,
+        description: description || undefined,
+        color,
+        workspaceId,
+        folderId: folderId || undefined,
+        sortOrder,
+      },
       request: req,
     })
 
diff --git a/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts b/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts
index 42711f1fa8c..3345888a6f7 100644
--- a/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts
@@ -97,7 +97,12 @@ export async function PUT(
       actorName: session.user.name ?? undefined,
       actorEmail: session.user.email ?? undefined,
       resourceName: name,
-      description: `Updated workspace API key: ${name}`,
+      description: `Renamed workspace API key from "${existingKey[0].name}" to "${name}"`,
+      metadata: {
+        keyType: 'workspace',
+        previousName: existingKey[0].name,
+        newName: name,
+      },
       request,
     })
 
@@ -163,7 +168,11 @@ export async function DELETE(
       actorEmail: session.user.email ?? undefined,
       resourceName: deletedKey.name,
       description: `Revoked workspace API key: ${deletedKey.name}`,
-      metadata: { lastUsed: deletedKey.lastUsed?.toISOString() ?? null },
+      metadata: {
+        keyType: 'workspace',
+        keyName: deletedKey.name,
+        lastUsed: deletedKey.lastUsed?.toISOString() ?? null,
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts
index a6a15bb52f2..3b5d851544d 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 ${keys.length} workspace API key(s)`,
+      metadata: { keyIds: keys, deletedCount, keyType: 'workspace' },
       request,
     })
 
diff --git a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts
index 65f177b1c55..5ccda1fae77 100644
--- a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts
@@ -172,6 +172,20 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
 
       logger.info(`[${requestId}] Updated BYOK key for ${providerId} in workspace ${workspaceId}`)
 
+      recordAudit({
+        workspaceId,
+        actorId: userId,
+        actorName: session?.user?.name,
+        actorEmail: session?.user?.email,
+        action: AuditAction.BYOK_KEY_UPDATED,
+        resourceType: AuditResourceType.BYOK_KEY,
+        resourceId: existingKey[0].id,
+        resourceName: providerId,
+        description: `Updated BYOK key for ${providerId}`,
+        metadata: { providerId },
+        request,
+      })
+
       return NextResponse.json({
         success: true,
         key: {
diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.ts b/apps/sim/app/api/workspaces/[id]/environment/route.ts
index 2e118b628d7..67b1eddeb7a 100644
--- a/apps/sim/app/api/workspaces/[id]/environment/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/environment/route.ts
@@ -140,8 +140,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
       action: AuditAction.ENVIRONMENT_UPDATED,
       resourceType: AuditResourceType.ENVIRONMENT,
       resourceId: workspaceId,
-      description: `Updated environment variables`,
-      metadata: { variableCount: Object.keys(variables).length },
+      description: `Updated ${Object.keys(variables).length} workspace environment variable(s)`,
+      metadata: {
+        variableCount: Object.keys(variables).length,
+        updatedKeys: Object.keys(variables),
+        totalKeysAfterUpdate: Object.keys(merged).length,
+      },
       request,
     })
 
@@ -217,6 +221,22 @@ export async function DELETE(
       actingUserId: userId,
     })
 
+    recordAudit({
+      workspaceId,
+      actorId: userId,
+      actorName: session?.user?.name,
+      actorEmail: session?.user?.email,
+      action: AuditAction.ENVIRONMENT_DELETED,
+      resourceType: AuditResourceType.ENVIRONMENT,
+      resourceId: workspaceId,
+      description: `Removed ${keys.length} workspace environment variable(s)`,
+      metadata: {
+        removedKeys: keys,
+        remainingKeysCount: Object.keys(current).length,
+      },
+      request,
+    })
+
     return NextResponse.json({ success: true })
   } catch (error: any) {
     logger.error(`[${requestId}] Workspace env DELETE error`, error)
diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts
index 24b5eb56cf0..179efc41d3f 100644
--- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts
@@ -69,7 +69,9 @@ export async function PUT(
       action: AuditAction.FILE_UPDATED,
       resourceType: AuditResourceType.FILE,
       resourceId: fileId,
+      resourceName: updatedFile.name,
       description: `Updated content of file "${updatedFile.name}"`,
+      metadata: { contentSize: buffer.length },
       request,
     })
 
diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts
index c440618863e..34cacc6808d 100644
--- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts
@@ -58,6 +58,7 @@ export async function PATCH(
       action: AuditAction.FILE_UPDATED,
       resourceType: AuditResourceType.FILE,
       resourceId: fileId,
+      resourceName: updatedFile.name,
       description: `Renamed file to "${updatedFile.name}"`,
       request,
     })
diff --git a/apps/sim/app/api/workspaces/[id]/files/route.ts b/apps/sim/app/api/workspaces/[id]/files/route.ts
index 5c887442796..41bdf82569f 100644
--- a/apps/sim/app/api/workspaces/[id]/files/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/files/route.ts
@@ -134,6 +134,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
       resourceId: userFile.id,
       resourceName: fileName,
       description: `Uploaded file "${fileName}"`,
+      metadata: { fileSize: rawFile.size, fileType: rawFile.type || 'application/octet-stream' },
       request,
     })
 
diff --git a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts
index 08d3f5802d2..ae5ae96c3e6 100644
--- a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts
@@ -262,6 +262,14 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
       actorName: session.user.name ?? undefined,
       actorEmail: session.user.email ?? undefined,
       description: `Updated ${subscription.notificationType} notification subscription`,
+      metadata: {
+        notificationType: subscription.notificationType,
+        updatedFields: Object.keys(data).filter(
+          (k) => (data as Record)[k] !== undefined
+        ),
+        ...(data.active !== undefined && { active: data.active }),
+        ...(data.alertConfig !== undefined && { alertRule: data.alertConfig?.rule ?? null }),
+      },
       request,
     })
 
@@ -340,6 +348,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
       actorEmail: session.user.email ?? undefined,
       resourceName: deletedSubscription.notificationType,
       description: `Deleted ${deletedSubscription.notificationType} notification subscription`,
+      metadata: {
+        notificationType: deletedSubscription.notificationType,
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/workspaces/[id]/notifications/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/route.ts
index 1a18f8d2386..3ad7532f8e8 100644
--- a/apps/sim/app/api/workspaces/[id]/notifications/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/notifications/route.ts
@@ -278,6 +278,17 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
       actorName: session.user.name ?? undefined,
       actorEmail: session.user.email ?? undefined,
       description: `Created ${data.notificationType} notification subscription`,
+      metadata: {
+        notificationType: data.notificationType,
+        allWorkflows: data.allWorkflows,
+        workflowCount: data.workflowIds.length,
+        levelFilter: data.levelFilter,
+        alertRule: data.alertConfig?.rule ?? null,
+        ...(data.notificationType === 'email' && {
+          recipientCount: data.emailRecipients?.length ?? 0,
+        }),
+        ...(data.notificationType === 'slack' && { channelName: data.slackConfig?.channelName }),
+      },
       request,
     })
 
diff --git a/apps/sim/app/api/workspaces/[id]/permissions/route.ts b/apps/sim/app/api/workspaces/[id]/permissions/route.ts
index f31bce34ba6..e7ee5385597 100644
--- a/apps/sim/app/api/workspaces/[id]/permissions/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/permissions/route.ts
@@ -202,19 +202,15 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
         action: AuditAction.MEMBER_ROLE_CHANGED,
         resourceType: AuditResourceType.WORKSPACE,
         resourceId: workspaceId,
+        resourceName: permLookup.get(update.userId)?.email ?? update.userId,
         actorName: session.user.name ?? undefined,
         actorEmail: session.user.email ?? undefined,
-        description: `Changed permissions for user ${update.userId} to ${update.permissions}`,
+        description: `Changed permissions for ${permLookup.get(update.userId)?.email ?? update.userId} from ${permLookup.get(update.userId)?.permission ?? 'none'} to ${update.permissions}`,
         metadata: {
           targetUserId: update.userId,
           targetEmail: permLookup.get(update.userId)?.email ?? undefined,
-          changes: [
-            {
-              field: 'permissions',
-              from: permLookup.get(update.userId)?.permission ?? null,
-              to: update.permissions,
-            },
-          ],
+          previousRole: permLookup.get(update.userId)?.permission ?? null,
+          newRole: update.permissions,
         },
         request,
       })
diff --git a/apps/sim/app/api/workspaces/[id]/route.ts b/apps/sim/app/api/workspaces/[id]/route.ts
index 375e0879b8b..ca4e9408fcf 100644
--- a/apps/sim/app/api/workspaces/[id]/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/route.ts
@@ -202,6 +202,37 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
       .where(eq(workspace.id, workspaceId))
       .then((rows) => rows[0])
 
+    recordAudit({
+      workspaceId,
+      actorId: session.user.id,
+      actorName: session.user.name,
+      actorEmail: session.user.email,
+      action: AuditAction.WORKSPACE_UPDATED,
+      resourceType: AuditResourceType.WORKSPACE,
+      resourceId: workspaceId,
+      resourceName: updatedWorkspace?.name ?? existingWorkspace.name,
+      description: `Updated workspace "${updatedWorkspace?.name ?? existingWorkspace.name}"`,
+      metadata: {
+        changes: {
+          ...(name !== undefined && { name: { from: existingWorkspace.name, to: name } }),
+          ...(color !== undefined && { color: { from: existingWorkspace.color, to: color } }),
+          ...(allowPersonalApiKeys !== undefined && {
+            allowPersonalApiKeys: {
+              from: existingWorkspace.allowPersonalApiKeys,
+              to: allowPersonalApiKeys,
+            },
+          }),
+          ...(billedAccountUserId !== undefined && {
+            billedAccountUserId: {
+              from: existingWorkspace.billedAccountUserId,
+              to: billedAccountUserId,
+            },
+          }),
+        },
+      },
+      request,
+    })
+
     return NextResponse.json({
       workspace: {
         ...updatedWorkspace,
diff --git a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts
index 602b60e88cd..d76322d4e5f 100644
--- a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts
+++ b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts
@@ -189,7 +189,13 @@ export async function GET(
         actorEmail: session.user.email ?? undefined,
         resourceName: workspaceDetails.name,
         description: `Accepted workspace invitation to "${workspaceDetails.name}"`,
-        metadata: { targetEmail: invitation.email },
+        metadata: {
+          targetEmail: invitation.email,
+          workspaceName: workspaceDetails.name,
+          assignedPermission: invitation.permissions || 'read',
+          invitationId: invitation.id,
+          inviterId: invitation.inviterId,
+        },
         request: req,
       })
 
@@ -272,7 +278,11 @@ export async function DELETE(
       actorName: session.user.name ?? undefined,
       actorEmail: session.user.email ?? undefined,
       description: `Revoked workspace invitation for ${invitation.email}`,
-      metadata: { invitationId, targetEmail: invitation.email },
+      metadata: {
+        invitationId,
+        targetEmail: invitation.email,
+        invitationStatus: invitation.status,
+      },
       request: _request,
     })
 
@@ -360,6 +370,24 @@ export async function POST(
       )
     }
 
+    recordAudit({
+      workspaceId: invitation.workspaceId,
+      actorId: session.user.id,
+      action: AuditAction.INVITATION_RESENT,
+      resourceType: AuditResourceType.WORKSPACE,
+      resourceId: invitation.workspaceId,
+      actorName: session.user.name ?? undefined,
+      actorEmail: session.user.email ?? undefined,
+      resourceName: ws.name,
+      description: `Resent workspace invitation to ${invitation.email}`,
+      metadata: {
+        invitationId,
+        targetEmail: invitation.email,
+        workspaceName: ws.name,
+      },
+      request: _request,
+    })
+
     return NextResponse.json({ success: true })
   } catch (error) {
     logger.error('Error resending workspace invitation:', error)
diff --git a/apps/sim/app/api/workspaces/invitations/route.ts b/apps/sim/app/api/workspaces/invitations/route.ts
index 30c91acd22f..020e350dbb2 100644
--- a/apps/sim/app/api/workspaces/invitations/route.ts
+++ b/apps/sim/app/api/workspaces/invitations/route.ts
@@ -243,7 +243,12 @@ export async function POST(req: NextRequest) {
       resourceId: workspaceId,
       resourceName: email,
       description: `Invited ${email} as ${permission}`,
-      metadata: { targetEmail: email, targetRole: permission },
+      metadata: {
+        targetEmail: email,
+        targetRole: permission,
+        workspaceName: workspaceDetails.name,
+        invitationId: invitationData.id,
+      },
       request: req,
     })
 
diff --git a/apps/sim/app/api/workspaces/members/[id]/route.ts b/apps/sim/app/api/workspaces/members/[id]/route.ts
index ca918712946..e4a507c5a78 100644
--- a/apps/sim/app/api/workspaces/members/[id]/route.ts
+++ b/apps/sim/app/api/workspaces/members/[id]/route.ts
@@ -121,8 +121,12 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
       action: AuditAction.MEMBER_REMOVED,
       resourceType: AuditResourceType.WORKSPACE,
       resourceId: workspaceId,
-      description: isSelf ? 'Left the workspace' : 'Removed a member from the workspace',
-      metadata: { removedUserId: userId, selfRemoval: isSelf },
+      description: isSelf ? 'Left the workspace' : `Removed member ${userId} from the workspace`,
+      metadata: {
+        removedUserId: userId,
+        removedUserRole: userPermission.permissionType,
+        selfRemoval: isSelf,
+      },
       request: req,
     })
 
diff --git a/apps/sim/app/api/workspaces/route.ts b/apps/sim/app/api/workspaces/route.ts
index d64fd05f758..1fdc15f94df 100644
--- a/apps/sim/app/api/workspaces/route.ts
+++ b/apps/sim/app/api/workspaces/route.ts
@@ -118,7 +118,7 @@ export async function POST(req: Request) {
       resourceId: newWorkspace.id,
       resourceName: newWorkspace.name,
       description: `Created workspace "${newWorkspace.name}"`,
-      metadata: { name: newWorkspace.name },
+      metadata: { name: newWorkspace.name, color: newWorkspace.color },
       request: req,
     })
 
diff --git a/apps/sim/ee/audit-logs/components/audit-logs.tsx b/apps/sim/ee/audit-logs/components/audit-logs.tsx
index 463f4e8e738..7aef8bd7946 100644
--- a/apps/sim/ee/audit-logs/components/audit-logs.tsx
+++ b/apps/sim/ee/audit-logs/components/audit-logs.tsx
@@ -7,33 +7,12 @@ import { Badge, Button, Combobox, type ComboboxOption, Skeleton } from '@/compon
 import { Input } from '@/components/ui'
 import { cn } from '@/lib/core/utils/cn'
 import { formatDateTime } from '@/lib/core/utils/formatting'
-import { AuditResourceType } from '@/lib/audit/log'
 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 ACRONYMS = new Set(['API', 'BYOK', 'MCP', 'OAuth'])
-
-function formatResourceLabel(key: string): string {
-  const words = key.split('_')
-  return words
-    .map((w) => {
-      const upper = w.toUpperCase()
-      if (ACRONYMS.has(upper)) return upper
-      return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
-    })
-    .join(' ')
-    .replace('OAUTH', 'OAuth')
-}
-
-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)),
-]
-
 const DATE_RANGE_OPTIONS: ComboboxOption[] = [
   { label: 'Last 7 days', value: '7' },
   { label: 'Last 30 days', value: '30' },
diff --git a/apps/sim/ee/audit-logs/constants.ts b/apps/sim/ee/audit-logs/constants.ts
new file mode 100644
index 00000000000..2c5939c6033
--- /dev/null
+++ b/apps/sim/ee/audit-logs/constants.ts
@@ -0,0 +1,23 @@
+import type { ComboboxOption } from '@/components/emcn'
+import { AuditResourceType } from '@/lib/audit/log'
+
+const ACRONYMS = new Set(['API', 'BYOK', 'MCP', 'OAuth'])
+
+function formatResourceLabel(key: string): string {
+  const words = key.split('_')
+  return words
+    .map((w) => {
+      const upper = w.toUpperCase()
+      if (ACRONYMS.has(upper)) return upper
+      return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
+    })
+    .join(' ')
+    .replace('OAUTH', 'OAuth')
+}
+
+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/lib/audit/log.ts b/apps/sim/lib/audit/log.ts
index 394b6f1cacf..3fb323d76e5 100644
--- a/apps/sim/lib/audit/log.ts
+++ b/apps/sim/lib/audit/log.ts
@@ -20,6 +20,7 @@ export const AuditAction = {
 
   // BYOK Keys
   BYOK_KEY_CREATED: 'byok_key.created',
+  BYOK_KEY_UPDATED: 'byok_key.updated',
   BYOK_KEY_DELETED: 'byok_key.deleted',
 
   // Chat
@@ -57,6 +58,7 @@ export const AuditAction = {
 
   // Environment
   ENVIRONMENT_UPDATED: 'environment.updated',
+  ENVIRONMENT_DELETED: 'environment.deleted',
 
   // Files
   FILE_UPLOADED: 'file.uploaded',
@@ -77,6 +79,7 @@ export const AuditAction = {
 
   // Invitations
   INVITATION_ACCEPTED: 'invitation.accepted',
+  INVITATION_RESENT: 'invitation.resent',
   INVITATION_REVOKED: 'invitation.revoked',
 
   // Knowledge Base Connectors
@@ -128,6 +131,7 @@ export const AuditAction = {
   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',
@@ -176,6 +180,7 @@ export const AuditAction = {
 
   // Workspaces
   WORKSPACE_CREATED: 'workspace.created',
+  WORKSPACE_UPDATED: 'workspace.updated',
   WORKSPACE_DELETED: 'workspace.deleted',
   WORKSPACE_DUPLICATED: 'workspace.duplicated',
 } as const
diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts
index 7fa61bf1bdd..b08d15d7431 100644
--- a/apps/sim/lib/auth/auth.ts
+++ b/apps/sim/lib/auth/auth.ts
@@ -707,6 +707,7 @@ export const auth = betterAuth({
         actorEmail: resetUser.email,
         action: AuditAction.PASSWORD_RESET,
         resourceType: AuditResourceType.PASSWORD,
+        resourceId: resetUser.id,
         description: `Password reset completed for ${resetUser.email}`,
       })
     },
diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts
index eb89c876cee..f75d6d7fbca 100644
--- a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts
+++ b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts
@@ -261,6 +261,7 @@ export async function executeDeployMcp(
         resourceType: AuditResourceType.MCP_SERVER,
         resourceId: serverId,
         description: `Undeployed workflow "${workflowId}" from MCP server`,
+        metadata: { workflowId, source: 'copilot' },
       })
 
       return {
@@ -324,6 +325,7 @@ export async function executeDeployMcp(
         resourceType: AuditResourceType.MCP_SERVER,
         resourceId: serverId,
         description: `Updated MCP tool "${toolName}" on server`,
+        metadata: { workflowId, toolName, source: 'copilot' },
       })
 
       return {
@@ -353,6 +355,7 @@ export async function executeDeployMcp(
       resourceType: AuditResourceType.MCP_SERVER,
       resourceId: serverId,
       description: `Deployed workflow as MCP tool "${toolName}"`,
+      metadata: { workflowId, toolName, toolId, source: 'copilot' },
     })
 
     return {
diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts
index 233b1cbfe25..00ecfce4dce 100644
--- a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts
+++ b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts
@@ -255,6 +255,11 @@ export async function executeCreateWorkspaceMcpServer(
       resourceId: serverId,
       resourceName: name,
       description: `Created MCP server "${name}"`,
+      metadata: {
+        isPublic: params.isPublic ?? false,
+        toolCount: addedTools.length,
+        source: 'copilot',
+      },
     })
 
     return { success: true, output: { server, addedTools } }
@@ -314,6 +319,10 @@ export async function executeUpdateWorkspaceMcpServer(
       resourceType: AuditResourceType.MCP_SERVER,
       resourceId: params.serverId,
       description: `Updated MCP server`,
+      metadata: {
+        updatedFields: Object.keys(updates).filter((k) => k !== 'updatedAt'),
+        source: 'copilot',
+      },
     })
 
     return { success: true, output: { serverId, ...updates, updatedAt: undefined } }
@@ -357,7 +366,9 @@ export async function executeDeleteWorkspaceMcpServer(
       action: AuditAction.MCP_SERVER_REMOVED,
       resourceType: AuditResourceType.MCP_SERVER,
       resourceId: params.serverId,
-      description: `Deleted MCP server`,
+      resourceName: existing.name,
+      description: `Deleted MCP server "${existing.name}"`,
+      metadata: { source: 'copilot' },
     })
 
     return { success: true, output: { serverId, name: existing.name, deleted: true } }
diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts
index 5cbbcdd5730..ac48a677081 100644
--- a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts
+++ b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts
@@ -241,6 +241,7 @@ async function executeManageCustomTool(
         resourceId: created?.id,
         resourceName: title,
         description: `Created custom tool "${title}"`,
+        metadata: { source: 'copilot' },
       })
 
       return {
@@ -299,6 +300,7 @@ async function executeManageCustomTool(
         resourceId: params.toolId,
         resourceName: title,
         description: `Updated custom tool "${title}"`,
+        metadata: { source: 'copilot' },
       })
 
       return {
@@ -334,6 +336,7 @@ async function executeManageCustomTool(
         resourceType: AuditResourceType.CUSTOM_TOOL,
         resourceId: params.toolId,
         description: 'Deleted custom tool',
+        metadata: { source: 'copilot' },
       })
 
       return {
@@ -502,6 +505,7 @@ async function executeManageMcpTool(
         description: existing
           ? `Updated existing MCP server "${config.name}"`
           : `Added MCP server "${config.name}"`,
+        metadata: { transport: config.transport, url: config.url, source: 'copilot' },
       })
 
       return {
@@ -563,7 +567,9 @@ async function executeManageMcpTool(
         action: AuditAction.MCP_SERVER_UPDATED,
         resourceType: AuditResourceType.MCP_SERVER,
         resourceId: params.serverId,
+        resourceName: updated.name,
         description: `Updated MCP server "${updated.name}"`,
+        metadata: { source: 'copilot' },
       })
 
       return {
@@ -607,7 +613,9 @@ async function executeManageMcpTool(
         action: AuditAction.MCP_SERVER_REMOVED,
         resourceType: AuditResourceType.MCP_SERVER,
         resourceId: params.serverId,
+        resourceName: deleted.name,
         description: `Deleted MCP server "${deleted.name}"`,
+        metadata: { source: 'copilot' },
       })
 
       return {
@@ -719,6 +727,7 @@ async function executeManageSkill(
         resourceId: created?.id,
         resourceName: params.name,
         description: `Created skill "${params.name}"`,
+        metadata: { source: 'copilot' },
       })
 
       return {
@@ -773,6 +782,7 @@ async function executeManageSkill(
         resourceId: params.skillId,
         resourceName: updatedName,
         description: `Updated skill "${updatedName}"`,
+        metadata: { source: 'copilot' },
       })
 
       return {
@@ -804,6 +814,7 @@ async function executeManageSkill(
         resourceType: AuditResourceType.SKILL,
         resourceId: params.skillId,
         description: 'Deleted skill',
+        metadata: { source: 'copilot' },
       })
 
       return {
@@ -1055,7 +1066,9 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
             action: AuditAction.CREDENTIAL_RENAMED,
             resourceType: AuditResourceType.OAUTH,
             resourceId: credentialId,
+            resourceName: displayName,
             description: `Renamed credential to "${displayName}"`,
+            metadata: { source: 'copilot' },
           })
           return { success: true, output: { credentialId, displayName } }
         }
@@ -1067,6 +1080,7 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
             resourceType: AuditResourceType.OAUTH,
             resourceId: credentialId,
             description: `Deleted credential`,
+            metadata: { source: 'copilot' },
           })
           return { success: true, output: { credentialId, deleted: true } }
         }
diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts
index c2a378cb768..615fcdee647 100644
--- a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts
+++ b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts
@@ -141,6 +141,7 @@ export async function executeCreateWorkflow(
       resourceId: result.workflowId,
       resourceName: name,
       description: `Created workflow "${name}"`,
+      metadata: { folderId, source: 'copilot' },
     })
 
     try {
@@ -216,6 +217,7 @@ export async function executeCreateFolder(
       resourceId: result.folderId,
       resourceName: name,
       description: `Created folder "${name}"`,
+      metadata: { parentId, source: 'copilot' },
     })
 
     return { success: true, output: result }
@@ -372,6 +374,7 @@ export async function executeSetGlobalWorkflowVariables(
       resourceType: AuditResourceType.WORKFLOW,
       resourceId: workflowId,
       description: `Updated workflow variables`,
+      metadata: { operationCount: operations.length, source: 'copilot' },
     })
 
     return { success: true, output: { updated: Object.values(byName).length } }
@@ -536,7 +539,10 @@ export async function executeGenerateApiKey(
       actorId: context.userId,
       action: AuditAction.API_KEY_CREATED,
       resourceType: AuditResourceType.API_KEY,
-      description: `Generated API key for workspace`,
+      resourceId: newKey.id,
+      resourceName: name,
+      description: `Generated API key "${name}" for workspace`,
+      metadata: { source: 'copilot' },
     })
 
     return {
diff --git a/apps/sim/lib/workflows/orchestration/chat-deploy.ts b/apps/sim/lib/workflows/orchestration/chat-deploy.ts
index 1a00e325d29..3c372a0aa82 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,12 @@ export async function performChatUndeploy(
     resourceId: chatId,
     resourceName: chatRecord.title || chatId,
     description: `Deleted chat deployment "${chatRecord.title || chatId}"`,
+    metadata: {
+      chatTitle: chatRecord.title || undefined,
+      workflowId: chatRecord.workflowId || undefined,
+      identifier: chatRecord.identifier || undefined,
+      authType: chatRecord.authType || undefined,
+    },
   })
 
   return { success: true }
diff --git a/apps/sim/lib/workflows/orchestration/deploy.ts b/apps/sim/lib/workflows/orchestration/deploy.ts
index 5e8863ccb0b..9ab543262eb 100644
--- a/apps/sim/lib/workflows/orchestration/deploy.ts
+++ b/apps/sim/lib/workflows/orchestration/deploy.ts
@@ -209,7 +209,13 @@ 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,
+      workflowName: (workflowData.name as string) || undefined,
+      previousVersionId: previousVersionId || undefined,
+      triggerWarnings: triggerSaveResult.warnings?.length ? triggerSaveResult.warnings : undefined,
+    },
     request,
   })
 
@@ -285,6 +291,9 @@ export async function performFullUndeploy(
     resourceId: workflowId,
     resourceName: (workflowData.name as string) || undefined,
     description: `Undeployed workflow "${(workflowData.name as string) || workflowId}"`,
+    metadata: {
+      workflowName: (workflowData.name as string) || undefined,
+    },
   })
 
   return { success: true }
@@ -473,7 +482,13 @@ 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,
+      workflowName: (workflow.name as string) || undefined,
+    },
   })
 
   return {
diff --git a/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts b/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts
index 3e757e6b2bf..535530c6722 100644
--- a/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts
+++ b/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts
@@ -109,8 +109,10 @@ export async function performDeleteWorkflow(
     resourceName: workflowRecord.name,
     description: `Archived workflow "${workflowRecord.name}"`,
     metadata: {
+      workflowName: workflowRecord.name,
       archived: archiveResult.archived,
       templateAction,
+      workspaceId: workflowRecord.workspaceId || undefined,
     },
   })
 
diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts
index 3665c5ce05b..a36f182738c 100644
--- a/packages/testing/src/mocks/audit.mock.ts
+++ b/packages/testing/src/mocks/audit.mock.ts
@@ -18,6 +18,7 @@ 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',
@@ -45,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',
@@ -57,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',
@@ -88,6 +91,7 @@ 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',
@@ -120,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',
   },

From e7d1d0c76e0a0b356c9b5eae9f84f3f39bc1459f Mon Sep 17 00:00:00 2001
From: Waleed Latif 
Date: Sat, 11 Apr 2026 13:25:10 -0700
Subject: [PATCH 13/17] 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 
---
 apps/sim/app/api/credential-sets/[id]/members/route.ts       | 1 -
 apps/sim/app/api/knowledge/[id]/documents/route.ts           | 3 ---
 .../api/workflows/[id]/deployments/[version]/revert/route.ts | 2 --
 apps/sim/app/api/workflows/[id]/duplicate/route.ts           | 2 --
 apps/sim/lib/workflows/orchestration/chat-deploy.ts          | 1 -
 apps/sim/lib/workflows/orchestration/deploy.ts               | 5 -----
 apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts   | 2 --
 7 files changed, 16 deletions(-)

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 aca1b63d648..8ec89923bbe 100644
--- a/apps/sim/app/api/credential-sets/[id]/members/route.ts
+++ b/apps/sim/app/api/credential-sets/[id]/members/route.ts
@@ -202,7 +202,6 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
         memberUserId: memberToRemove.userId,
         targetEmail: memberToRemove.email ?? undefined,
         providerId: result.set.providerId,
-        credentialSetName: result.set.name,
       },
       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 24ef30e603b..b5614aec41d 100644
--- a/apps/sim/app/api/knowledge/[id]/documents/route.ts
+++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts
@@ -280,8 +280,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
           metadata: {
             knowledgeBaseName: accessCheck.knowledgeBase?.name,
             fileCount: createdDocuments.length,
-            fileNames: createdDocuments.map((doc) => doc.filename),
-            uploadType: 'bulk',
           },
           request: req,
         })
@@ -364,7 +362,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
             fileName: validatedData.filename,
             fileType: validatedData.mimeType,
             fileSize: validatedData.fileSize,
-            uploadType: 'single',
           },
           request: req,
         })
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 c5b783b25ec..a2fb4fe4ba4 100644
--- a/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts
+++ b/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts
@@ -129,8 +129,6 @@ export async function POST(
       description: `Reverted workflow to deployment version ${version}`,
       metadata: {
         targetVersion: version,
-        workflowName: workflowRecord?.name ?? undefined,
-        workspaceId: workflowRecord?.workspaceId ?? undefined,
       },
       request,
     })
diff --git a/apps/sim/app/api/workflows/[id]/duplicate/route.ts b/apps/sim/app/api/workflows/[id]/duplicate/route.ts
index b421161a664..0af8a82bae0 100644
--- a/apps/sim/app/api/workflows/[id]/duplicate/route.ts
+++ b/apps/sim/app/api/workflows/[id]/duplicate/route.ts
@@ -90,8 +90,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
       metadata: {
         sourceWorkflowId,
         newWorkflowId: result.id,
-        newWorkflowName: result.name,
-        workspaceId: workspaceId || undefined,
         folderId: folderId || undefined,
       },
       request: req,
diff --git a/apps/sim/lib/workflows/orchestration/chat-deploy.ts b/apps/sim/lib/workflows/orchestration/chat-deploy.ts
index 3c372a0aa82..ada57d50641 100644
--- a/apps/sim/lib/workflows/orchestration/chat-deploy.ts
+++ b/apps/sim/lib/workflows/orchestration/chat-deploy.ts
@@ -213,7 +213,6 @@ export async function performChatUndeploy(
     resourceName: chatRecord.title || chatId,
     description: `Deleted chat deployment "${chatRecord.title || chatId}"`,
     metadata: {
-      chatTitle: chatRecord.title || undefined,
       workflowId: chatRecord.workflowId || undefined,
       identifier: chatRecord.identifier || undefined,
       authType: chatRecord.authType || undefined,
diff --git a/apps/sim/lib/workflows/orchestration/deploy.ts b/apps/sim/lib/workflows/orchestration/deploy.ts
index 9ab543262eb..d8709e47d50 100644
--- a/apps/sim/lib/workflows/orchestration/deploy.ts
+++ b/apps/sim/lib/workflows/orchestration/deploy.ts
@@ -212,7 +212,6 @@ export async function performFullDeploy(
     metadata: {
       deploymentVersionId,
       version: deployResult.version,
-      workflowName: (workflowData.name as string) || undefined,
       previousVersionId: previousVersionId || undefined,
       triggerWarnings: triggerSaveResult.warnings?.length ? triggerSaveResult.warnings : undefined,
     },
@@ -291,9 +290,6 @@ export async function performFullUndeploy(
     resourceId: workflowId,
     resourceName: (workflowData.name as string) || undefined,
     description: `Undeployed workflow "${(workflowData.name as string) || workflowId}"`,
-    metadata: {
-      workflowName: (workflowData.name as string) || undefined,
-    },
   })
 
   return { success: true }
@@ -487,7 +483,6 @@ export async function performActivateVersion(
       version,
       deploymentVersionId: versionRow.id,
       previousVersionId: previousVersionId || undefined,
-      workflowName: (workflow.name as string) || undefined,
     },
   })
 
diff --git a/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts b/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts
index 535530c6722..3e757e6b2bf 100644
--- a/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts
+++ b/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts
@@ -109,10 +109,8 @@ export async function performDeleteWorkflow(
     resourceName: workflowRecord.name,
     description: `Archived workflow "${workflowRecord.name}"`,
     metadata: {
-      workflowName: workflowRecord.name,
       archived: archiveResult.archived,
       templateAction,
-      workspaceId: workflowRecord.workspaceId || undefined,
     },
   })
 

From 231df15f391d0845634f466734d9687af84b22e8 Mon Sep 17 00:00:00 2001
From: Waleed Latif 
Date: Sat, 11 Apr 2026 13:28:06 -0700
Subject: [PATCH 14/17] 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 
---
 apps/sim/ee/audit-logs/constants.ts |   2 +-
 apps/sim/lib/audit/log.ts           | 219 +---------------------------
 apps/sim/lib/audit/types.ts         | 214 +++++++++++++++++++++++++++
 3 files changed, 219 insertions(+), 216 deletions(-)
 create mode 100644 apps/sim/lib/audit/types.ts

diff --git a/apps/sim/ee/audit-logs/constants.ts b/apps/sim/ee/audit-logs/constants.ts
index 2c5939c6033..a6613dcf888 100644
--- a/apps/sim/ee/audit-logs/constants.ts
+++ b/apps/sim/ee/audit-logs/constants.ts
@@ -1,5 +1,5 @@
 import type { ComboboxOption } from '@/components/emcn'
-import { AuditResourceType } from '@/lib/audit/log'
+import { AuditResourceType } from '@/lib/audit/types'
 
 const ACRONYMS = new Set(['API', 'BYOK', 'MCP', 'OAuth'])
 
diff --git a/apps/sim/lib/audit/log.ts b/apps/sim/lib/audit/log.ts
index 3fb323d76e5..ea7783aba78 100644
--- a/apps/sim/lib/audit/log.ts
+++ b/apps/sim/lib/audit/log.ts
@@ -2,225 +2,14 @@ import { auditLog, db } from '@sim/db'
 import { user } from '@sim/db/schema'
 import { createLogger } from '@sim/logger'
 import { eq } from 'drizzle-orm'
+import type { AuditActionType, AuditResourceTypeValue } from '@/lib/audit/types'
 import { getClientIp } from '@/lib/core/utils/request'
 import { generateShortId } from '@/lib/core/utils/uuid'
 
-const logger = createLogger('AuditLog')
-
-/**
- * All auditable actions in the platform, grouped by resource type.
- */
-export const AuditAction = {
-  // API Keys
-  API_KEY_CREATED: 'api_key.created',
-  API_KEY_UPDATED: 'api_key.updated',
-  API_KEY_REVOKED: 'api_key.revoked',
-  PERSONAL_API_KEY_CREATED: 'personal_api_key.created',
-  PERSONAL_API_KEY_REVOKED: 'personal_api_key.revoked',
-
-  // BYOK Keys
-  BYOK_KEY_CREATED: 'byok_key.created',
-  BYOK_KEY_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',
+export type { AuditActionType, AuditResourceTypeValue } from '@/lib/audit/types'
+export { AuditAction, AuditResourceType } from '@/lib/audit/types'
 
-  // Members
-  MEMBER_INVITED: 'member.invited',
-  MEMBER_REMOVED: 'member.removed',
-  MEMBER_ROLE_CHANGED: 'member.role_changed',
-
-  // Notifications
-  NOTIFICATION_CREATED: 'notification.created',
-  NOTIFICATION_UPDATED: 'notification.updated',
-  NOTIFICATION_DELETED: 'notification.deleted',
-
-  // OAuth / Credentials
-  OAUTH_DISCONNECTED: 'oauth.disconnected',
-  CREDENTIAL_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]
+const logger = createLogger('AuditLog')
 
 interface AuditLogParams {
   workspaceId?: string | null
diff --git a/apps/sim/lib/audit/types.ts b/apps/sim/lib/audit/types.ts
new file mode 100644
index 00000000000..bc1f857f469
--- /dev/null
+++ b/apps/sim/lib/audit/types.ts
@@ -0,0 +1,214 @@
+/**
+ * All auditable actions in the platform, grouped by resource type.
+ */
+export const AuditAction = {
+  // API Keys
+  API_KEY_CREATED: 'api_key.created',
+  API_KEY_UPDATED: 'api_key.updated',
+  API_KEY_REVOKED: 'api_key.revoked',
+  PERSONAL_API_KEY_CREATED: 'personal_api_key.created',
+  PERSONAL_API_KEY_REVOKED: 'personal_api_key.revoked',
+
+  // BYOK Keys
+  BYOK_KEY_CREATED: 'byok_key.created',
+  BYOK_KEY_UPDATED: 'byok_key.updated',
+  BYOK_KEY_DELETED: 'byok_key.deleted',
+
+  // Chat
+  CHAT_DEPLOYED: 'chat.deployed',
+  CHAT_UPDATED: 'chat.updated',
+  CHAT_DELETED: 'chat.deleted',
+
+  // Custom Tools
+  CUSTOM_TOOL_CREATED: 'custom_tool.created',
+  CUSTOM_TOOL_UPDATED: 'custom_tool.updated',
+  CUSTOM_TOOL_DELETED: 'custom_tool.deleted',
+
+  // Billing
+  CREDIT_PURCHASED: 'credit.purchased',
+
+  // Credential Sets
+  CREDENTIAL_SET_CREATED: 'credential_set.created',
+  CREDENTIAL_SET_UPDATED: 'credential_set.updated',
+  CREDENTIAL_SET_DELETED: 'credential_set.deleted',
+  CREDENTIAL_SET_MEMBER_REMOVED: 'credential_set_member.removed',
+  CREDENTIAL_SET_MEMBER_LEFT: 'credential_set_member.left',
+  CREDENTIAL_SET_INVITATION_CREATED: 'credential_set_invitation.created',
+  CREDENTIAL_SET_INVITATION_ACCEPTED: 'credential_set_invitation.accepted',
+  CREDENTIAL_SET_INVITATION_RESENT: 'credential_set_invitation.resent',
+  CREDENTIAL_SET_INVITATION_REVOKED: 'credential_set_invitation.revoked',
+
+  // Connector Documents
+  CONNECTOR_DOCUMENT_RESTORED: 'connector_document.restored',
+  CONNECTOR_DOCUMENT_EXCLUDED: 'connector_document.excluded',
+
+  // Documents
+  DOCUMENT_UPLOADED: 'document.uploaded',
+  DOCUMENT_UPDATED: 'document.updated',
+  DOCUMENT_DELETED: 'document.deleted',
+
+  // Environment
+  ENVIRONMENT_UPDATED: 'environment.updated',
+  ENVIRONMENT_DELETED: 'environment.deleted',
+
+  // Files
+  FILE_UPLOADED: 'file.uploaded',
+  FILE_UPDATED: 'file.updated',
+  FILE_DELETED: 'file.deleted',
+  FILE_RESTORED: 'file.restored',
+
+  // Folders
+  FOLDER_CREATED: 'folder.created',
+  FOLDER_DELETED: 'folder.deleted',
+  FOLDER_DUPLICATED: 'folder.duplicated',
+  FOLDER_RESTORED: 'folder.restored',
+
+  // Forms
+  FORM_CREATED: 'form.created',
+  FORM_UPDATED: 'form.updated',
+  FORM_DELETED: 'form.deleted',
+
+  // Invitations
+  INVITATION_ACCEPTED: 'invitation.accepted',
+  INVITATION_RESENT: 'invitation.resent',
+  INVITATION_REVOKED: 'invitation.revoked',
+
+  // Knowledge Base Connectors
+  CONNECTOR_CREATED: 'connector.created',
+  CONNECTOR_UPDATED: 'connector.updated',
+  CONNECTOR_DELETED: 'connector.deleted',
+  CONNECTOR_SYNCED: 'connector.synced',
+
+  // Knowledge Bases
+  KNOWLEDGE_BASE_CREATED: 'knowledge_base.created',
+  KNOWLEDGE_BASE_UPDATED: 'knowledge_base.updated',
+  KNOWLEDGE_BASE_DELETED: 'knowledge_base.deleted',
+  KNOWLEDGE_BASE_RESTORED: 'knowledge_base.restored',
+
+  // MCP Servers
+  MCP_SERVER_ADDED: 'mcp_server.added',
+  MCP_SERVER_UPDATED: 'mcp_server.updated',
+  MCP_SERVER_REMOVED: 'mcp_server.removed',
+
+  // Members
+  MEMBER_INVITED: 'member.invited',
+  MEMBER_REMOVED: 'member.removed',
+  MEMBER_ROLE_CHANGED: 'member.role_changed',
+
+  // Notifications
+  NOTIFICATION_CREATED: 'notification.created',
+  NOTIFICATION_UPDATED: 'notification.updated',
+  NOTIFICATION_DELETED: 'notification.deleted',
+
+  // OAuth / Credentials
+  OAUTH_DISCONNECTED: 'oauth.disconnected',
+  CREDENTIAL_CREATED: 'credential.created',
+  CREDENTIAL_UPDATED: 'credential.updated',
+  CREDENTIAL_RENAMED: 'credential.renamed',
+  CREDENTIAL_DELETED: 'credential.deleted',
+
+  // Password
+  PASSWORD_RESET_REQUESTED: 'password.reset_requested',
+  PASSWORD_RESET: 'password.reset',
+
+  // Organizations
+  ORGANIZATION_CREATED: 'organization.created',
+  ORGANIZATION_UPDATED: 'organization.updated',
+  ORG_MEMBER_ADDED: 'org_member.added',
+  ORG_MEMBER_REMOVED: 'org_member.removed',
+  ORG_MEMBER_ROLE_CHANGED: 'org_member.role_changed',
+  ORG_INVITATION_CREATED: 'org_invitation.created',
+  ORG_INVITATION_ACCEPTED: 'org_invitation.accepted',
+  ORG_INVITATION_REJECTED: 'org_invitation.rejected',
+  ORG_INVITATION_CANCELLED: 'org_invitation.cancelled',
+  ORG_INVITATION_REVOKED: 'org_invitation.revoked',
+  ORG_INVITATION_RESENT: 'org_invitation.resent',
+
+  // Permission Groups
+  PERMISSION_GROUP_CREATED: 'permission_group.created',
+  PERMISSION_GROUP_UPDATED: 'permission_group.updated',
+  PERMISSION_GROUP_DELETED: 'permission_group.deleted',
+  PERMISSION_GROUP_MEMBER_ADDED: 'permission_group_member.added',
+  PERMISSION_GROUP_MEMBER_REMOVED: 'permission_group_member.removed',
+
+  // Skills
+  SKILL_CREATED: 'skill.created',
+  SKILL_UPDATED: 'skill.updated',
+  SKILL_DELETED: 'skill.deleted',
+
+  // Schedules
+  SCHEDULE_CREATED: 'schedule.created',
+  SCHEDULE_UPDATED: 'schedule.updated',
+  SCHEDULE_DELETED: 'schedule.deleted',
+
+  // Tables
+  TABLE_CREATED: 'table.created',
+  TABLE_UPDATED: 'table.updated',
+  TABLE_DELETED: 'table.deleted',
+  TABLE_RESTORED: 'table.restored',
+
+  // Templates
+  TEMPLATE_CREATED: 'template.created',
+  TEMPLATE_UPDATED: 'template.updated',
+  TEMPLATE_DELETED: 'template.deleted',
+
+  // Webhooks
+  WEBHOOK_CREATED: 'webhook.created',
+  WEBHOOK_DELETED: 'webhook.deleted',
+
+  // Workflows
+  WORKFLOW_CREATED: 'workflow.created',
+  WORKFLOW_DELETED: 'workflow.deleted',
+  WORKFLOW_RESTORED: 'workflow.restored',
+  WORKFLOW_DEPLOYED: 'workflow.deployed',
+  WORKFLOW_UNDEPLOYED: 'workflow.undeployed',
+  WORKFLOW_DUPLICATED: 'workflow.duplicated',
+  WORKFLOW_DEPLOYMENT_ACTIVATED: 'workflow.deployment_activated',
+  WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted',
+  WORKFLOW_LOCKED: 'workflow.locked',
+  WORKFLOW_UNLOCKED: 'workflow.unlocked',
+  WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated',
+
+  // Workspaces
+  WORKSPACE_CREATED: 'workspace.created',
+  WORKSPACE_UPDATED: 'workspace.updated',
+  WORKSPACE_DELETED: 'workspace.deleted',
+  WORKSPACE_DUPLICATED: 'workspace.duplicated',
+} as const
+
+export type AuditActionType = (typeof AuditAction)[keyof typeof AuditAction]
+
+/**
+ * All resource types that can appear in audit log entries.
+ */
+export const AuditResourceType = {
+  API_KEY: 'api_key',
+  BILLING: 'billing',
+  BYOK_KEY: 'byok_key',
+  CHAT: 'chat',
+  CONNECTOR: 'connector',
+  CREDENTIAL: 'credential',
+  CREDENTIAL_SET: 'credential_set',
+  CUSTOM_TOOL: 'custom_tool',
+  DOCUMENT: 'document',
+  ENVIRONMENT: 'environment',
+  FILE: 'file',
+  FOLDER: 'folder',
+  FORM: 'form',
+  KNOWLEDGE_BASE: 'knowledge_base',
+  MCP_SERVER: 'mcp_server',
+  NOTIFICATION: 'notification',
+  OAUTH: 'oauth',
+  ORGANIZATION: 'organization',
+  PASSWORD: 'password',
+  PERMISSION_GROUP: 'permission_group',
+  SCHEDULE: 'schedule',
+  SKILL: 'skill',
+  TABLE: 'table',
+  TEMPLATE: 'template',
+  WEBHOOK: 'webhook',
+  WORKFLOW: 'workflow',
+  WORKSPACE: 'workspace',
+} as const
+
+export type AuditResourceTypeValue = (typeof AuditResourceType)[keyof typeof AuditResourceType]

From 6f60475f097377c830fceca5303529f11428fff0 Mon Sep 17 00:00:00 2001
From: Waleed Latif 
Date: Sat, 11 Apr 2026 14:09:01 -0700
Subject: [PATCH 15/17] 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 
---
 apps/sim/app/api/v1/audit-logs/query.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/apps/sim/app/api/v1/audit-logs/query.ts b/apps/sim/app/api/v1/audit-logs/query.ts
index db187a218c6..14e24c65427 100644
--- a/apps/sim/app/api/v1/audit-logs/query.ts
+++ b/apps/sim/app/api/v1/audit-logs/query.ts
@@ -45,7 +45,8 @@ export function buildFilterConditions(params: AuditLogFilterParams): SQL
Date: Sat, 11 Apr 2026 14:57:02 -0700
Subject: [PATCH 16/17] 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 
---
 apps/sim/app/api/workspaces/[id]/api-keys/route.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 3b5d851544d..4c156d06f94 100644
--- a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts
@@ -257,7 +257,7 @@ export async function DELETE(
       actorEmail: session?.user?.email,
       action: AuditAction.API_KEY_REVOKED,
       resourceType: AuditResourceType.API_KEY,
-      description: `Revoked ${keys.length} workspace API key(s)`,
+      description: `Revoked ${deletedCount} workspace API key(s)`,
       metadata: { keyIds: keys, deletedCount, keyType: 'workspace' },
       request,
     })

From 4a996f4a3459b737db394c20dcfe23a62c4f9cb2 Mon Sep 17 00:00:00 2001
From: Waleed Latif 
Date: Sat, 11 Apr 2026 15:04:11 -0700
Subject: [PATCH 17/17] 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 
---
 apps/sim/ee/audit-logs/constants.ts | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/apps/sim/ee/audit-logs/constants.ts b/apps/sim/ee/audit-logs/constants.ts
index a6613dcf888..445265f4f81 100644
--- a/apps/sim/ee/audit-logs/constants.ts
+++ b/apps/sim/ee/audit-logs/constants.ts
@@ -1,18 +1,19 @@
 import type { ComboboxOption } from '@/components/emcn'
 import { AuditResourceType } from '@/lib/audit/types'
 
-const ACRONYMS = new Set(['API', 'BYOK', 'MCP', 'OAuth'])
+const ACRONYMS = new Set(['API', 'BYOK', 'MCP', 'OAUTH'])
+
+const DISPLAY_OVERRIDES: Record = { OAUTH: 'OAuth' }
 
 function formatResourceLabel(key: string): string {
-  const words = key.split('_')
-  return words
+  return key
+    .split('_')
     .map((w) => {
       const upper = w.toUpperCase()
-      if (ACRONYMS.has(upper)) return upper
+      if (ACRONYMS.has(upper)) return DISPLAY_OVERRIDES[upper] ?? upper
       return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
     })
     .join(' ')
-    .replace('OAUTH', 'OAuth')
 }
 
 export const RESOURCE_TYPE_OPTIONS: ComboboxOption[] = [