feat(custom-block): move management to enterprise settings; derive inputs live from deployed start#5453
feat(custom-block): move management to enterprise settings; derive inputs live from deployed start#5453TheodoreSpeaks wants to merge 7 commits into
Conversation
…puts live from deployed start
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview API & persistence: Publish/update accept Runtime & UX: Disabled blocks stay in the client/server overlay ( Reviewed by Cursor Bugbot for commit 70b8ed1. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
@greptile review |
| inputs: inputPlaceholders, | ||
| exposedOutputs, | ||
| ...(iconUrl ? { iconUrl } : {}), | ||
| }) |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 237fc05. Configure here.
| notDeployed || | ||
| iconUpload.isUploading || | ||
| deployed.isLoading || | ||
| (deployedLoaded && visibleOutputs.length === 0) |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 34108b0. Configure here.
Greptile SummaryThis 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.
Confidence Score: 3/5Safe 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
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
%%{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
|
| 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, | ||
| })) | ||
| ) |
There was a problem hiding this comment.
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 SummaryThis 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.
Confidence Score: 3/5The 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 apps/sim/hooks/queries/custom-blocks.ts — the Important Files Changed
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)
%%{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)
|
…re inputs by authored data only
…ResourceRow + shared reserved-param set
There was a problem hiding this comment.
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).
❌ 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.
…s fail loudly instead of vanishing


Summary
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 lingerType of Change
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, andcheck:migrations origin/stagingall pass.Checklist