Skip to content

feat(custom-block): move management to enterprise settings; derive inputs live from deployed start#5453

Open
TheodoreSpeaks wants to merge 7 commits into
stagingfrom
feat/custom-block-settings
Open

feat(custom-block): move management to enterprise settings; derive inputs live from deployed start#5453
TheodoreSpeaks wants to merge 7 commits into
stagingfrom
feat/custom-block-settings

Conversation

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator

Summary

  • Move custom-block creation/editing out of the workflow Deploy modal into a dedicated Enterprise settings page (pick workspace → deployed workflow → per-input placeholders → curated outputs → icon)
  • Store only per-input placeholders in custom_block.inputs; always derive the input field set (name/type) live from the deployed Start, so an input removed from the source can't linger
  • Icon chain: uploaded icon → org whitelabel logo → default glyph; fix image-icon sizing in canvas/toolbar/command palette
  • Harden lifecycle: deleting a custom block no longer corrupts consuming workflows (dropped like a removed block); deleted inputs are never passed to the child; object/array inputs decode correctly (no double-encoding)

Type of Change

  • New feature + bug fixes

Testing

Tested manually against a live deployed workflow. Added unit tests: serializer drops a deleted custom block + its edges; handler remap drops non-existent fields and decodes json inputs. bun run lint, check:api-validation:strict, and check:migrations origin/staging all pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 7, 2026 1:44am

Request Review

@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches workflow serialization, child-workflow execution, and org-wide block resolution—behavior changes for disabled/deleted blocks and input mapping—but is covered by new unit tests and mostly confines risk to custom-block consumers.

Overview
Custom blocks are no longer published from the Deploy modal’s Block tab (removed); admins manage them under a new Enterprise → Custom blocks settings section (NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED), with list/create/edit flows that pick workspace and deployed workflow, require at least one curated output, and support per-input placeholders only.

API & persistence: Publish/update accept inputs (placeholder overrides by Start field id); list responses add workflowName / workspaceName. Migration adds custom_block.inputs json. Input name/type/description always come from the live deployed Start; stored placeholders are merged at read time. React Query invalidates all custom-block lists after mutations.

Runtime & UX: Disabled blocks stay in the client/server overlay (hideFromToolbar) so existing canvas instances serialize instead of vanishing; palette/toolbar still hide disabled blocks for new placement. Icons fall back upload → whitelabel logo → default, with sizing fixes on tiles. Serializer drops unresolvable custom blocks and stale input values when the config declares inputs; workflow handler decodes object/array JSON inputs and fails clearly when a custom block’s source workflow is undeployed.

Reviewed by Cursor Bugbot for commit 70b8ed1. Bugbot is set up for automated code reviews on this repo. Configure here.

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

Comment thread apps/sim/hooks/queries/custom-blocks.ts Outdated
Comment thread apps/sim/ee/custom-blocks/components/custom-block-detail.tsx
inputs: inputPlaceholders,
exposedOutputs,
...(iconUrl ? { iconUrl } : {}),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cross-org publish invisible

Medium Severity

Create flow lists every workspace from useWorkspacesQuery and publishes using selectedWorkspaceId, while the settings list always loads custom blocks for the URL workspace’s organization. An admin who picks a workspace in another org gets a success toast but the new block never appears in the list they’re viewing.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 237fc05. Configure here.

notDeployed ||
iconUpload.isUploading ||
deployed.isLoading ||
(deployedLoaded && visibleOutputs.length === 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Legacy blocks cannot update

Medium Severity

The detail view requires at least one curated output to enable Save and rejects empty exposedOutputs. Blocks published with no outputs (previously “all outputs / result”) keep exposedOutputs: [] and cannot be edited or updated in the new settings UI.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 34108b0. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR migrates custom-block creation/editing out of the workflow Deploy modal into a dedicated Enterprise settings page, refactors input storage to only persist per-field placeholder overrides (deriving the full field set live from the deployed Start), fixes two lifecycle bugs (deleted blocks no longer crash serialization; deleted inputs are no longer passed to the child workflow), and hardens icon rendering across canvas/toolbar/palette.

  • Settings UI: New CustomBlocks + CustomBlockDetail components in the Enterprise settings section; workspace \u2192 workflow picker on create, live input derivation from the deployed state, output curation, and unsaved-changes guard.
  • Serializer hardening: Unresolvable custom blocks (type deleted) are now silently dropped with their edges during both serialize and deserialize; deleted inputs filtered out for blocks with declared configs.
  • Handler fix: remapCustomBlockInputKeys maps stable field IDs to current names, drops removed fields, and JSON-decodes object/array values to prevent double-encoding before the child receives them.

Confidence Score: 3/5

Safe to merge after fixing the query-key regression; the core execution, serialization, and settings-page changes are correct and well-tested.

The query key in hooks/queries/custom-blocks.ts was changed from list(workspaceId) to lists(), removing workspaceId from the cache discriminator while the fetch still scopes by it. Any user switching between workspaces from different organizations will see the previous org's custom blocks for up to 60 seconds. The CustomBlocksLoader comment explicitly relied on the key changing on workspace switch — that invariant is now broken. All other changes look correct.

apps/sim/hooks/queries/custom-blocks.ts — query key must be restored to customBlockKeys.list(workspaceId)

Important Files Changed

Filename Overview
apps/sim/hooks/queries/custom-blocks.ts Query key changed from list(workspaceId) to lists(), removing workspaceId from the cache key while the fetch still scopes by workspaceId — causes cross-workspace cache collisions for multi-org users.
apps/sim/executor/handlers/workflow/workflow-handler.ts Adds remapCustomBlockInputKeys: maps stable field IDs to current names, drops removed fields, and JSON-decodes object/array inputs to prevent double-encoding. Well-tested and correct.
apps/sim/serializer/index.ts Deleted custom blocks now silently drop with their edges instead of crashing serialization; deleted inputs filtered for blocks with declared configs. Covered by new lifecycle tests.
apps/sim/lib/workflows/custom-blocks/operations.ts Input fields now derived live from the deployed Start; stored placeholders merged via applyInputPlaceholders. Publish/update/list APIs updated. Authorization boundaries intact.
apps/sim/ee/custom-blocks/components/custom-block-detail.tsx New Enterprise settings UI for creating/editing custom blocks. Derives inputs live from deployed state, stores only per-input placeholders. Unsaved-changes guard and delete confirmation present.
apps/sim/ee/custom-blocks/components/custom-blocks.tsx List view for custom blocks in Enterprise settings; correctly gates on enterprise plan and feature flag.
apps/sim/lib/api/contracts/custom-blocks.ts Added inputs, workflowName, and workspaceName to contracts; Zod schemas and route contracts correctly updated.
packages/db/migrations/0255_custom_block_inputs.sql Adds nullable json column inputs to custom_block — additive, non-breaking migration.
apps/sim/blocks/custom/custom-block-icon.tsx New getCustomBlockIcon helper with icon chain: uploaded icon → org whitelabel logo → default glyph. Module-level URL cache prevents duplicate component creation.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Admin as Enterprise Admin
    participant Settings as Settings UI
    participant API as /api/custom-blocks
    participant Ops as operations.ts
    participant DB as Database
    participant Deploy as Deployed State

    Admin->>Settings: Create block
    Settings->>Deploy: useDeployedWorkflowState(workflowId)
    Deploy-->>Settings: blocks with Start inputs
    Settings->>Settings: extractInputFieldsFromBlocks
    Admin->>Settings: Set placeholders + curate outputs
    Settings->>API: POST /api/custom-blocks
    API->>Ops: publishCustomBlock
    Ops->>DB: INSERT custom_block
    Ops->>Deploy: deriveInputFields(workflowId)
    Deploy-->>Ops: Live Start fields
    Ops-->>API: CustomBlockWithInputs
    API-->>Settings: 200 OK

    participant Executor as WorkflowBlockHandler
    Executor->>Ops: getCustomBlockAuthority(type, workspaceId)
    Ops-->>Executor: workflowId + ownerUserId
    Executor->>Deploy: loadChildWorkflowDeployed
    Deploy-->>Executor: deployedState
    Executor->>Executor: remapCustomBlockInputKeys
    Executor->>Executor: run child under owner identity
    Executor-->>Executor: projectCustomBlockOutput
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Admin as Enterprise Admin
    participant Settings as Settings UI
    participant API as /api/custom-blocks
    participant Ops as operations.ts
    participant DB as Database
    participant Deploy as Deployed State

    Admin->>Settings: Create block
    Settings->>Deploy: useDeployedWorkflowState(workflowId)
    Deploy-->>Settings: blocks with Start inputs
    Settings->>Settings: extractInputFieldsFromBlocks
    Admin->>Settings: Set placeholders + curate outputs
    Settings->>API: POST /api/custom-blocks
    API->>Ops: publishCustomBlock
    Ops->>DB: INSERT custom_block
    Ops->>Deploy: deriveInputFields(workflowId)
    Deploy-->>Ops: Live Start fields
    Ops-->>API: CustomBlockWithInputs
    API-->>Settings: 200 OK

    participant Executor as WorkflowBlockHandler
    Executor->>Ops: getCustomBlockAuthority(type, workspaceId)
    Ops-->>Executor: workflowId + ownerUserId
    Executor->>Deploy: loadChildWorkflowDeployed
    Deploy-->>Executor: deployedState
    Executor->>Executor: remapCustomBlockInputKeys
    Executor->>Executor: run child under owner identity
    Executor-->>Executor: projectCustomBlockOutput
Loading

Comments Outside Diff (1)

  1. apps/sim/hooks/queries/custom-blocks.ts, line 34 (link)

    P1 Query key drops workspaceId — cross-workspace cache collision

    The key was changed from customBlockKeys.list(workspaceId) to customBlockKeys.lists(), but the queryFn still fetches with workspaceId as a scoping argument. Per the project's React Query rules, every identifier forwarded into the fetch must appear in the query key — otherwise distinct workspaceIds share one cache entry.

    Concretely: a user who switches from Org-A's workspace to Org-B's workspace within a 60-second window (the staleTime) will see Org-A's custom blocks in the palette, toolbar, and canvas of Org-B's workspace. The CustomBlocksLoader comment even documents the expected "Re-hydrates on workspace switch (the query key changes)" — that invariant is now broken. Fix: keep customBlockKeys.list(workspaceId) as the query key; the invalidations can remain customBlockKeys.lists() for broad coverage.

Reviews (1): Last reviewed commit: "fix(custom-block): key custom-blocks que..." | Re-trigger Greptile

Comment on lines +95 to +103
const [inputs, setInputs] = useState<CustomBlockInput[]>(() =>
(existing?.inputFields ?? []).map((f) => ({
id: f.id ?? f.name,
name: f.name,
type: f.type,
placeholder: f.placeholder,
description: f.description,
}))
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 inputs state initialized from stale existing.inputFields may diverge before mount

The inputs useState initializer captures existing.inputFields synchronously at mount, but existing is derived from a React Query result. If the query hasn't resolved yet (fresh mount, no cache), existing is null and inputs starts as []. Once the query resolves and existing populates, the state is NOT reset — only visibleInputs is live-derived via the deployed-state memo. In practice this is fine for editing (the detail mounts with the correct key={selected} after the list loads), but the lazy initialization comment would benefit from a note explaining why this is safe rather than silently relying on the parent's key reset.

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR moves custom-block management out of the Deploy modal into a dedicated Enterprise Settings page, stores only per-input placeholder hints in the DB (deriving the full input field set live from the deployed Start block so removed inputs can never linger), hardens the lifecycle so deleted custom blocks are dropped gracefully instead of corrupting workflows, and fixes image-icon sizing across the canvas, toolbar, and command palette.

  • New Enterprise Settings page (custom-block-detail.tsx, custom-blocks.tsx): create/edit/delete custom blocks; input fields always reflect the latest deployment; only authored data (placeholders) is persisted.
  • Lifecycle hardening: the serializer now drops unresolvable custom blocks and their edges on both serialize/deserialize; extractBlockParams filters deleted inputs for blocks whose config declares its input set; remapCustomBlockInputKeys JSON-decodes object/array inputs and drops keys with no matching child field.
  • DB schema: a nullable inputs JSON column is added to custom_block via a non-destructive migration; the inputs column stores Array<{id, placeholder?}> only.

Confidence Score: 3/5

The lifecycle and serializer changes are well-tested and correct, but the query-key change introduces a cross-workspace cache issue that should be resolved before merging.

The mutation invalidations rightly use customBlockKeys.lists() (the prefix), but the query in useCustomBlocksQuery was also switched to that same prefix key, dropping workspaceId from the cache entry. fetchCustomBlocks still passes workspaceId to the API, so two workspaces from different organisations share one React Query cache slot. During the 60-second stale window, a user who belongs to multiple org workspaces would see stale data from the wrong org — and the project's enforced key-fetch-arg-drift CI check exists specifically to catch this. Everything else in the PR — the DB migration, serializer hardening, handler remap, icon sizing, and the new settings UI — looks solid.

apps/sim/hooks/queries/custom-blocks.ts — the queryKey for useCustomBlocksQuery must include workspaceId

Important Files Changed

Filename Overview
apps/sim/hooks/queries/custom-blocks.ts Query key changed from workspace-scoped customBlockKeys.list(workspaceId) to prefix-level customBlockKeys.lists(), causing cross-workspace cache collisions when the same user is a member of multiple organisations' workspaces
apps/sim/executor/handlers/workflow/workflow-handler.ts Custom block deployment check moved outside the inner try block (intentional: prevents generic sanitiser from masking the human-readable error message); object/array inputs are now JSON-decoded before passing to the child; checkChildDeployment is called exactly once per path
apps/sim/serializer/index.ts Deleted custom blocks are now gracefully dropped (with their edges) on both serialize and deserialize instead of throwing Invalid block type; deleted inputs on live blocks with declared configs are also filtered out
apps/sim/lib/workflows/custom-blocks/operations.ts Input fields now derived live from the deployed Start block with stored placeholders merged in; workflowName and workspaceName joined from related tables for display
apps/sim/ee/custom-blocks/components/custom-block-detail.tsx New 659-line settings UI for creating/editing custom blocks; derives inputs from deployed Start state, authors only per-field placeholders, and guards unsaved navigation
packages/db/schema.ts Adds nullable inputs JSON column to custom_block for per-field placeholder storage; migration is a single non-destructive ALTER TABLE ADD COLUMN
apps/sim/blocks/custom/custom-block-icon.tsx Image icon now applies size-full last in cn() so it fills a tile container; !important modifiers removed from toolbar/command-palette consumers intentionally so size-full wins for image icons while non-image glyphs retain the small size; fallback chain added (uploaded → whitelabel logo → default glyph)
apps/sim/serializer/custom-block-lifecycle.test.ts New test file covering both bugs: deleted input not leaking into inputMapping, and deleted custom block being dropped gracefully on serialize/deserialize

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Admin as Admin (Settings UI)
    participant API as /api/custom-blocks
    participant DB as custom_block (DB)
    participant Start as Deployed Start Block
    participant Consumer as Consumer Workflow

    Admin->>API: "POST publish {workflowId, name, inputs:[{id,placeholder}], exposedOutputs}"
    API->>DB: "INSERT custom_block (inputs=placeholders only)"
    API->>Start: deriveInputFields(workflowId)
    Start-->>API: "[{id,name,type}...]"
    API-->>Admin: block with live inputFields (placeholders merged)

    Admin->>API: "PATCH update {inputs, exposedOutputs}"
    API->>DB: UPDATE inputs column

    Consumer->>API: GET /api/custom-blocks
    API->>DB: SELECT custom_block JOIN workflow JOIN workspace
    API->>Start: deriveInputFields per enabled block
    Start-->>API: live field set
    API-->>Consumer: "inputFields = live fields + stored placeholders merged"

    Consumer->>Consumer: serialize workflow
    Note over Consumer: isCustomBlockType + !getBlock → drop block + edges
    Consumer->>Consumer: remapCustomBlockInputKeys (drop stale, decode JSON)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Admin as Admin (Settings UI)
    participant API as /api/custom-blocks
    participant DB as custom_block (DB)
    participant Start as Deployed Start Block
    participant Consumer as Consumer Workflow

    Admin->>API: "POST publish {workflowId, name, inputs:[{id,placeholder}], exposedOutputs}"
    API->>DB: "INSERT custom_block (inputs=placeholders only)"
    API->>Start: deriveInputFields(workflowId)
    Start-->>API: "[{id,name,type}...]"
    API-->>Admin: block with live inputFields (placeholders merged)

    Admin->>API: "PATCH update {inputs, exposedOutputs}"
    API->>DB: UPDATE inputs column

    Consumer->>API: GET /api/custom-blocks
    API->>DB: SELECT custom_block JOIN workflow JOIN workspace
    API->>Start: deriveInputFields per enabled block
    Start-->>API: live field set
    API-->>Consumer: "inputFields = live fields + stored placeholders merged"

    Consumer->>Consumer: serialize workflow
    Note over Consumer: isCustomBlockType + !getBlock → drop block + edges
    Consumer->>Consumer: remapCustomBlockInputKeys (drop stale, decode JSON)
Loading

Comments Outside Diff (1)

  1. apps/sim/hooks/queries/custom-blocks.ts, line 35-41 (link)

    P1 workspaceId in queryFn but absent from queryKey

    fetchCustomBlocks(workspaceId, ...) is scoped by workspaceId, but the key is now customBlockKeys.lists() (no workspace segment). Two calls with different workspaceId values — e.g., a user who belongs to workspaces from two different organisations — share one cache entry: the second workspace's query returns the first workspace's custom blocks until the 60-second stale window expires. The project's sim-queries.md rule ("Every identifier the queryFn forwards into the fetch MUST appear in the queryKey") and the enforced key-fetch-arg-drift CI check both forbid this pattern.

    The mutation hooks correctly use customBlockKeys.lists() for invalidation (that prefix covers all workspace-specific entries), but the query key must be the workspace-specific form customBlockKeys.list(workspaceId).

Reviews (2): Last reviewed commit: "fix(custom-block): key custom-blocks que..." | Re-trigger Greptile

Comment thread apps/sim/ee/custom-blocks/components/custom-block-detail.tsx Outdated
Comment thread apps/sim/ee/custom-blocks/components/custom-block-detail.tsx
Comment thread apps/sim/ee/custom-blocks/components/custom-block-detail.tsx
Comment thread apps/sim/lib/workflows/custom-blocks/operations.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 75e0c89. Configure here.

Comment thread apps/sim/serializer/index.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant