Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
515b50d
fix(app): keep square logos out of loading screens (#8897)
waleedlatif1 Oct 10, 2026
654f0ca
fix(inbox): scope reply threading to the receiving workspace (#8895)
waleedlatif1 Oct 10, 2026
25f5cd4
fix(realtime): scope batch-add upserts to the caller's workflow (#8894)
waleedlatif1 Oct 10, 2026
dc9654d
fix(file-parsers): cap untrusted number formats and make time-format …
waleedlatif1 Oct 10, 2026
71f05ec
perf(search): search up to eight live accounts at once (#8891)
waleedlatif1 Oct 10, 2026
c1c5ae0
perf(search): fetch Lucid page regions concurrently on a live read (#…
waleedlatif1 Oct 10, 2026
3492598
feat(projects): move Project membership to the workspace column (#8830)
mzxchandra Oct 10, 2026
558d96d
fix(search): return actionable live read errors and drop unreadable C…
waleedlatif1 Oct 10, 2026
5551eb6
perf(files): resolve name references through an index instead of list…
waleedlatif1 Oct 11, 2026
fd91c62
perf(knowledge): lock the knowledge base row FOR NO KEY UPDATE on doc…
waleedlatif1 Oct 11, 2026
a12f59b
perf(auth): stop blocking requests on best-effort last-used timestamp…
waleedlatif1 Oct 11, 2026
8599648
perf(tables): seek keyset row pages instead of scanning from the firs…
waleedlatif1 Oct 11, 2026
c66a36a
docs(library): update best-ai-agents-for-regulated-industry-workflows…
icecrasher321 Oct 11, 2026
756f651
docs(library): update best-ai-agents-for-customer-support-automation …
icecrasher321 Oct 11, 2026
1d27adc
docs(library): update best-ai-agents-sales-crm-automation (#8901)
icecrasher321 Oct 11, 2026
a20b981
docs(library): update what-is-an-mcp-server (#8902)
icecrasher321 Oct 11, 2026
30d7a87
feat(library): How to Build a Pipedrive CRM AI Agent for Deal Follow-…
icecrasher321 Oct 11, 2026
089f3ec
docs(library): update ai-agent-observability (#8900)
icecrasher321 Oct 11, 2026
3684920
feat(library): How Do You Build a HubSpot AI Agent for Lead Enrichmen…
icecrasher321 Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/docs/content/docs/platform/enterprise/whitelabeling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ Go to **Settings → Organization → White-labeling** in your workspace.
| Field | Description |
|-------|-------------|
| **Logo** | Shown in the collapsed sidebar. Square image (PNG, JPEG, SVG, or WebP). Max 5 MB. |
| **Wordmark** | Shown in the expanded sidebar. Wide image (PNG, JPEG, SVG, or WebP). Max 5 MB. |
| **Wordmark** | Shown in the expanded sidebar and application loading screens. Wide image (PNG, JPEG, SVG, or WebP). Max 5 MB. |
| **Brand name** | Replaces "Sim" in the sidebar and select UI elements. Max 64 characters. |

Loading screens use the wordmark, never the square logo. Without a wordmark, they show the brand name; when the name is still Sim, they use the built-in Sim wordmark.

### 3. Configure colors

Expand Down
210 changes: 99 additions & 111 deletions apps/realtime/src/database/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ import {
normalizeWorkflowEdgeSourceHandle,
normalizeWorkflowEdgeTargetHandle,
} from '@sim/workflow-types/workflow'
import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm'
import { and, eq, inArray, isNull, or, type SQL, sql } from 'drizzle-orm'
import type { PgInsertValue } from 'drizzle-orm/pg-core'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { env } from '@/env'
Expand Down Expand Up @@ -77,6 +78,45 @@ function canonicalizeEdgeAddCandidate(edge: EdgeAddCandidate): EdgeAddCandidate
}
}

type WorkflowScopedTable = typeof workflowBlocks | typeof workflowEdges | typeof workflowSubflows

/**
* Inserts rows into a workflow-scoped table, updating rows that already exist
* in `workflowId`. Row ids are unique across every workflow, so an upsert keyed
* on `id` alone would overwrite another workflow's row: `setWhere` confines
* the update to this workflow, and a conflict it skips leaves a row unreturned,
* which aborts the transaction instead of dropping that row silently.
*/
async function upsertWorkflowRows<T extends WorkflowScopedTable>(
tx: DbOrTx,
table: T,
workflowId: string,
values: PgInsertValue<T>[],
set: Partial<Record<keyof T['$inferInsert'], SQL>>
) {
const written = await tx
.insert(table)
.values(values)
.onConflictDoUpdate({ target: table.id, set, setWhere: eq(table.workflowId, workflowId) })
.returning({ id: table.id })

if (written.length !== values.length) {
throw new Error('One or more ids already belong to another workflow')
}
}

const SUBFLOW_UPSERT_SET = {
config: sql`excluded.config`,
updatedAt: sql`now()`,
} as const

const EDGE_UPSERT_SET = {
sourceBlockId: sql`excluded.source_block_id`,
targetBlockId: sql`excluded.target_block_id`,
sourceHandle: sql`excluded.source_handle`,
targetHandle: sql`excluded.target_handle`,
} as const

interface FilterEdgesForPersistResult<T> {
safeEdges: T[]
droppedCounts: Record<string, number>
Expand Down Expand Up @@ -949,8 +989,8 @@ async function handleBlocksOperationTx(
workflowId,
type: block.type as string,
name: block.name as string,
positionX: (block.position as { x: number; y: number }).x,
positionY: (block.position as { x: number; y: number }).y,
positionX: String((block.position as { x: number; y: number }).x),
positionY: String((block.position as { x: number; y: number }).y),
data: (block.data as Record<string, unknown> | undefined) || {},
subBlocks: mergedSubBlocks,
outputs: (block.outputs as Record<string, unknown>) || {},
Expand All @@ -960,84 +1000,66 @@ async function handleBlocksOperationTx(
triggerMode: (block.triggerMode as boolean) ?? false,
errorEnabled: (block.errorEnabled as boolean) ?? false,
retry: (block.retry as Record<string, unknown> | undefined) ?? null,
height: (block.height as number) || 0,
height: String((block.height as number) || 0),
locked: (block.locked as boolean) ?? false,
}
})

await tx
.insert(workflowBlocks)
.values(blockValues)
.onConflictDoUpdate({
target: workflowBlocks.id,
set: {
type: sql`excluded.type`,
name: sql`excluded.name`,
positionX: sql`excluded.position_x`,
positionY: sql`excluded.position_y`,
enabled: sql`excluded.enabled`,
horizontalHandles: sql`excluded.horizontal_handles`,
advancedMode: sql`excluded.advanced_mode`,
triggerMode: sql`excluded.trigger_mode`,
errorEnabled: sql`excluded.error_enabled`,
retry: sql`excluded.retry`,
locked: sql`excluded.locked`,
height: sql`excluded.height`,
subBlocks: sql`excluded.sub_blocks`,
outputs: sql`excluded.outputs`,
data: sql`excluded.data`,
updatedAt: sql`now()`,
},
})
await upsertWorkflowRows(tx, workflowBlocks, workflowId, blockValues, {
type: sql`excluded.type`,
name: sql`excluded.name`,
positionX: sql`excluded.position_x`,
positionY: sql`excluded.position_y`,
enabled: sql`excluded.enabled`,
horizontalHandles: sql`excluded.horizontal_handles`,
advancedMode: sql`excluded.advanced_mode`,
triggerMode: sql`excluded.trigger_mode`,
errorEnabled: sql`excluded.error_enabled`,
retry: sql`excluded.retry`,
locked: sql`excluded.locked`,
height: sql`excluded.height`,
subBlocks: sql`excluded.sub_blocks`,
outputs: sql`excluded.outputs`,
data: sql`excluded.data`,
updatedAt: sql`now()`,
})

// Create subflow entries for loop/parallel blocks (skip if already in payload)
const loopIds = new Set(loops ? Object.keys(loops) : [])
const parallelIds = new Set(parallels ? Object.keys(parallels) : [])
const defaultSubflows = []
for (const block of allowedBlocks) {
const blockId = block.id as string
if (block.type === 'loop' && !loopIds.has(blockId)) {
await tx
.insert(workflowSubflows)
.values({
id: blockId,
workflowId,
type: 'loop',
config: {
loopType: 'for',
iterations: DEFAULT_LOOP_ITERATIONS,
nodes: [],
},
})
.onConflictDoUpdate({
target: workflowSubflows.id,
set: {
config: sql`excluded.config`,
updatedAt: sql`now()`,
},
})
defaultSubflows.push({
id: blockId,
workflowId,
type: 'loop',
config: { loopType: 'for', iterations: DEFAULT_LOOP_ITERATIONS, nodes: [] },
})
} else if (block.type === 'parallel' && !parallelIds.has(blockId)) {
await tx
.insert(workflowSubflows)
.values({
id: blockId,
workflowId,
type: 'parallel',
config: {
parallelType: 'count',
count: DEFAULT_PARALLEL_COUNT,
batchSize: DEFAULT_PARALLEL_BATCH_SIZE,
nodes: [],
},
})
.onConflictDoUpdate({
target: workflowSubflows.id,
set: {
config: sql`excluded.config`,
updatedAt: sql`now()`,
},
})
defaultSubflows.push({
id: blockId,
workflowId,
type: 'parallel',
config: {
parallelType: 'count',
count: DEFAULT_PARALLEL_COUNT,
batchSize: DEFAULT_PARALLEL_BATCH_SIZE,
nodes: [],
},
})
}
}
if (defaultSubflows.length > 0) {
await upsertWorkflowRows(
tx,
workflowSubflows,
workflowId,
defaultSubflows,
SUBFLOW_UPSERT_SET
)
}

const parentIds = new Set<string>()
for (const block of allowedBlocks) {
Expand Down Expand Up @@ -1087,18 +1109,7 @@ async function handleBlocksOperationTx(
targetHandle: normalizeWorkflowEdgeTargetHandle(edge.targetHandle),
}))

await tx
.insert(workflowEdges)
.values(edgeValues)
.onConflictDoUpdate({
target: workflowEdges.id,
set: {
sourceBlockId: sql`excluded.source_block_id`,
targetBlockId: sql`excluded.target_block_id`,
sourceHandle: sql`excluded.source_handle`,
targetHandle: sql`excluded.target_handle`,
},
})
await upsertWorkflowRows(tx, workflowEdges, workflowId, edgeValues, EDGE_UPSERT_SET)
}
}

Expand All @@ -1110,16 +1121,7 @@ async function handleBlocksOperationTx(
config: loop as Record<string, unknown>,
}))

await tx
.insert(workflowSubflows)
.values(loopValues)
.onConflictDoUpdate({
target: workflowSubflows.id,
set: {
config: sql`excluded.config`,
updatedAt: sql`now()`,
},
})
await upsertWorkflowRows(tx, workflowSubflows, workflowId, loopValues, SUBFLOW_UPSERT_SET)
}

if (parallels && Object.keys(parallels).length > 0) {
Expand All @@ -1130,16 +1132,13 @@ async function handleBlocksOperationTx(
config: parallel as Record<string, unknown>,
}))

await tx
.insert(workflowSubflows)
.values(parallelValues)
.onConflictDoUpdate({
target: workflowSubflows.id,
set: {
config: sql`excluded.config`,
updatedAt: sql`now()`,
},
})
await upsertWorkflowRows(
tx,
workflowSubflows,
workflowId,
parallelValues,
SUBFLOW_UPSERT_SET
)
}

logger.info(`Successfully batch added blocks to workflow ${workflowId}`)
Expand Down Expand Up @@ -1791,18 +1790,7 @@ async function handleEdgesOperationTx(
targetHandle: normalizeWorkflowEdgeTargetHandle(edge.targetHandle),
}))

await tx
.insert(workflowEdges)
.values(edgeValues)
.onConflictDoUpdate({
target: workflowEdges.id,
set: {
sourceBlockId: sql`excluded.source_block_id`,
targetBlockId: sql`excluded.target_block_id`,
sourceHandle: sql`excluded.source_handle`,
targetHandle: sql`excluded.target_handle`,
},
})
await upsertWorkflowRows(tx, workflowEdges, workflowId, edgeValues, EDGE_UPSERT_SET)

logger.debug(`Batch added ${safeEdges.length} edges to workflow ${workflowId}`)
break
Expand Down
9 changes: 4 additions & 5 deletions apps/sim/app/_shell/application-loading.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cn, SimWordmark } from '@sim/emcn'
import { defaultBrandConfig } from '@/lib/branding'
import { DesktopTitleBarLane } from '@/app/_shell/desktop-title-bar'
import { type BrandConfig, getBrandConfig } from '@/ee/whitelabeling'

Expand All @@ -12,8 +13,6 @@ export function ApplicationLoading({
brand = getBrandConfig(),
fullScreen = true,
}: ApplicationLoadingProps) {
const wordmarkUrl = brand.wordmarkUrl || brand.logoUrl

return (
<div
role='status'
Expand All @@ -24,9 +23,9 @@ export function ApplicationLoading({
)}
>
{fullScreen && <DesktopTitleBarLane />}
{wordmarkUrl ? (
<img src={wordmarkUrl} alt='' className='h-8 max-w-[240px] object-contain' />
) : brand.isWhitelabeled ? (
{brand.wordmarkUrl ? (
<img src={brand.wordmarkUrl} alt='' className='h-8 max-w-[240px] object-contain' />
) : brand.name !== defaultBrandConfig.name ? (
<span className='text-[var(--text-tertiary)] text-lg'>{brand.name}</span>
) : (
<SimWordmark size='loading' tone='brand-muted' />
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/v1/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export async function authenticateV1Request(request: NextRequest): Promise<AuthR
}
}

await updateApiKeyLastUsed(result.keyId)
updateApiKeyLastUsed(result.keyId)

return {
authenticated: true,
Expand Down
16 changes: 14 additions & 2 deletions apps/sim/app/api/webhooks/agentmail/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,20 @@ export const POST = withRouteHandler(async (req: Request) => {
const emailMessageId = message.message_id
const inReplyTo = message.in_reply_to || null

// Message ids are sender-controlled and shared with every recipient of a mail,
// so a task is only ever matched inside the workspace that owns this inbox.
const [existingResult, isAllowed, recentCount, parentTaskResult, isEntitled] =
await Promise.all([
emailMessageId
? db
.select({ id: mothershipInboxTask.id })
.from(mothershipInboxTask)
.where(eq(mothershipInboxTask.emailMessageId, emailMessageId))
.where(
and(
eq(mothershipInboxTask.workspaceId, result.id),
eq(mothershipInboxTask.emailMessageId, emailMessageId)
)
)
.limit(1)
: Promise.resolve([]),
isSenderAllowed(fromEmail, result.id),
Expand All @@ -200,7 +207,12 @@ export const POST = withRouteHandler(async (req: Request) => {
? db
.select({ chatId: mothershipInboxTask.chatId })
.from(mothershipInboxTask)
.where(eq(mothershipInboxTask.responseMessageId, inReplyTo))
.where(
and(
eq(mothershipInboxTask.workspaceId, result.id),
eq(mothershipInboxTask.responseMessageId, inReplyTo)
)
)
.limit(1)
: Promise.resolve([]),
hasWorkspaceInboxAccess(result.id),
Expand Down
Loading
Loading