Skip to content

feat(custom-block): deploy a workflow as a reusable org-scoped block - #5407

Merged
TheodoreSpeaks merged 15 commits into
stagingfrom
feat/custom-block
Jul 4, 2026
Merged

feat(custom-block): deploy a workflow as a reusable org-scoped block#5407
TheodoreSpeaks merged 15 commits into
stagingfrom
feat/custom-block

Conversation

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator

Summary

  • Publish a deployed workflow as a reusable block, scoped to the org and usable across every workspace like a native block
  • Start-block input fields become the block's inputs (anchored on stable field ids so renaming a field never orphans a placed value); curated, renamable outputs
  • Runs the source workflow's latest deployment under the owner's authority (invocation boundary) — cross-workspace consumers need no access to the source workflow
  • Gated by the deploy-as-block feature flag; org admins disable it via Access Control like any other block
  • Exposed to the copilot: VFS block files + a Custom Blocks section in the workspace context + an edit_workflow/get_blocks_metadata registry overlay (the typed VFS snapshot is untouched, so the Go diff is unaffected)
  • Billing: the child run's hosted-key cost rolls onto the block so the org is charged exactly as if the workflow ran directly
  • New custom_block table (migration 0254 — additive, backward-compatible)

Type of Change

  • New feature

Testing

  • Tested manually
  • bun run lint, bun run check:api-validation:strict, and bun run check:migrations origin/staging all pass
  • 170 unit tests pass (build-config, overlay, serializer, input-format, copilot VFS/context)

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

vercelBot commented Jul 4, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
docsSkippedSkippedJul 4, 2026 7:45pm

Request Review

@cursor

cursorBot commented Jul 4, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes workflow execution, cross-workspace authorization, and billing aggregation for a new block type touching the executor, APIs, and org-scoped data.

Overview
Adds deploy-as-block: enterprise orgs can publish a deployed workflow as a custom_block_* tile usable in any workspace, gated by the deploy-as-block flag and new custom_block persistence (migration 0254).

API & publish UX:GET/POST/api/custom-blocks and PATCH/DELETE/api/custom-blocks/[id] list and manage blocks (workspace access, admin on the source workflow’s workspace, enterprise + flag). The deploy modal gains a Block tab to set name, icon, description, and curated outputs from the deployed graph (publish / update / unpublish).

Registry overlay: Client hydration (CustomBlocksLoader) and server AsyncLocalStorage (withCustomBlockOverlay) extend getBlock / getAllBlocks so dynamic configs appear in the toolbar (Custom Blocks section), canvas, access-control block lists, and execution serialization. Start inputs become sub-blocks keyed on stable field ids; input-format now preserves those ids for rename-safe wiring.

Execution:WorkflowBlockHandler treats custom blocks as an invocation boundary—workflow id and authority come from the DB, the child always runs the latest deployment under the source owner’s user/workspace/env (not the consumer’s), inputs are remapped id→name, and success/failure surfaces only curated outputs or generic errors while hosted-key child cost still rolls up for billing. executeWorkflowCore and pre-run serialize paths wrap the org overlay; logs/trace label custom blocks without leaking internal workflowId/inputMapping.

Copilot: Org blocks materialize as VFS components/blocks/*.json, a Custom Blocks section in workspace markdown, and overlay-wrapped edit_workflow / get_blocks_metadata (agent schemas hide workflow_executor plumbing).

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

Comment threadapps/sim/executor/handlers/workflow/workflow-handler.ts
Comment threadapps/sim/lib/workflows/custom-blocks/operations.ts
@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@greptile review

@greptile-apps

greptile-appsBot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR implements "Deploy as Block" — a new Enterprise/org-scoped feature that lets an org admin publish any deployed workflow as a reusable block available across every workspace in the org, gated by the deploy-as-block feature flag. The custom block runs the source workflow's latest deployment under the source owner's identity (invocation boundary), so cross-workspace consumers need no access to the source workflow's credentials or environment.

  • New custom_block table (migration 0254, additive): stores type slug, bound workflowId, curated outputs, and enabled flag; FK cascades on workflow/org deletion prevent orphaned blocks.
  • Block execution path: WorkflowBlockHandler.execute now dispatches custom_block_* types through getCustomBlockAuthority (org-scoped DB lookup), remaps consumer input keys from stable field IDs to current field names, runs the child with the owner's env/credentials, and projects curated outputs via projectCustomBlockOutput.
  • Overlay architecture: a per-request AsyncLocalStorage-backed server overlay injects custom block configs into the synchronous @/blocks/registry accessors, keeping all existing callers (serializer, executor, copilot tools) unchanged; the client overlay hydrates the block palette on mount via CustomBlocksLoader.

Confidence Score: 5/5

The new execution path correctly enforces org-scoping at the authority lookup, applies owner-identity substitution, and cleans up child workflow outputs before returning them to the consumer — no new blocking issues found.

The invocation boundary model is implemented correctly across the authority lookup, handler, and overlay layers. New findings are quality-of-life concerns that do not affect correctness or security.

apps/sim/lib/workflows/custom-blocks/operations.ts for the N+1 query pattern in the list path; apps/sim/executor/handlers/workflow/workflow-handler.ts for the reserved output name collision noted in a prior review round.

Important Files Changed

FilenameOverview
apps/sim/lib/workflows/custom-blocks/operations.tsCore DB operations for custom blocks: publish/update/delete, authority lookup, and list with live-derived input fields. Authority lookup correctly scopes to the consumer's org. listCustomBlocksWithInputs fires N concurrent loadWorkflowFromNormalizedTables queries.
apps/sim/executor/handlers/workflow/workflow-handler.tsExtends WorkflowBlockHandler for custom block execution: authority lookup, cross-workspace boundary skip, owner-identity substitution, field-ID to name remapping, and curated output projection. projectCustomBlockOutput initializes output = { success: true } then writes exposed output names, so a name of "success" or "error" would overwrite the sentinel fields.
apps/sim/blocks/custom/server-overlay.tsPer-request AsyncLocalStorage overlay making custom_block_* types resolvable through the synchronous registry accessors. Clean, well-isolated design.
apps/sim/blocks/custom/build-config.tsSynthesizes BlockConfig from a DB row and live input fields. Uses stable field IDs as sub-block IDs for rename-safe wiring.
packages/db/schema.tsNew custom_block table with correct FK cascades. Unique index on (organization_id, type) but no unique constraint on (organization_id, workflow_id).
apps/sim/app/api/custom-blocks/route.tsGET returns all blocks so the client can filter. POST validates admin access, org membership, enterprise plan, and feature flag before publishing.
apps/sim/app/api/custom-blocks/[id]/route.tsPATCH/DELETE handlers with correct authz order — loads block first to derive org, then checks feature flag and admin permission.
apps/sim/app/workspace/[workspaceId]/providers/custom-blocks-loader.tsxClient-side hydrator. Correctly filters by enabled before calling hydrateClientCustomBlocks.
apps/sim/lib/workflows/executor/execution-core.tsWraps execution with the custom-block overlay at the shared choke point for all execution paths (sync route + background job).
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/block/block.tsxBlockDeploy form handling publish/update/unpublish. Icon upload reuses workspace-logos storage context. Dirty comparison for outputs uses untrimmed local state against trimmed server data.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Consumer as Consumer Workspace
participant Handler as WorkflowBlockHandler
participant Auth as getCustomBlockAuthority
participant DB as Database
participant OwnerEnv as Owner Env/Identity
participant Child as Child Executor
Consumer->>Handler: execute(custom_block_xyz block)
Handler->>Auth: getCustomBlockAuthority(type, consumerWorkspaceId)
Auth->>DB: getWorkspaceWithOwner(consumerWorkspaceId)
Auth->>DB: "SELECT custom_block JOIN workflow WHERE type=? AND org=?"
DB-->>Auth: workflowId, ownerUserId, exposedOutputs
Auth-->>Handler: authority
Handler->>DB: checkChildDeployment(workflowId, ownerUserId)
Handler->>DB: loadChildWorkflowDeployed(workflowId, ownerUserId)
Handler->>Handler: remapCustomBlockInputKeys(mapping, childBlocks)
Handler->>OwnerEnv: getPersonalAndWorkspaceEnv(ownerUserId, ownerWorkspaceId)
OwnerEnv-->>Handler: personalDecrypted + workspaceDecrypted
Handler->>Child: new Executor(serializedState, ownerEnvVars, ownerUserId)
Child-->>Handler: ExecutionResult
Handler->>Handler: projectCustomBlockOutput(result, exposedOutputs, childCost)
Handler-->>Consumer: BlockOutput success + namedOutputs
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 Consumer as Consumer Workspace
participant Handler as WorkflowBlockHandler
participant Auth as getCustomBlockAuthority
participant DB as Database
participant OwnerEnv as Owner Env/Identity
participant Child as Child Executor
Consumer->>Handler: execute(custom_block_xyz block)
Handler->>Auth: getCustomBlockAuthority(type, consumerWorkspaceId)
Auth->>DB: getWorkspaceWithOwner(consumerWorkspaceId)
Auth->>DB: "SELECT custom_block JOIN workflow WHERE type=? AND org=?"
DB-->>Auth: workflowId, ownerUserId, exposedOutputs
Auth-->>Handler: authority
Handler->>DB: checkChildDeployment(workflowId, ownerUserId)
Handler->>DB: loadChildWorkflowDeployed(workflowId, ownerUserId)
Handler->>Handler: remapCustomBlockInputKeys(mapping, childBlocks)
Handler->>OwnerEnv: getPersonalAndWorkspaceEnv(ownerUserId, ownerWorkspaceId)
OwnerEnv-->>Handler: personalDecrypted + workspaceDecrypted
Handler->>Child: new Executor(serializedState, ownerEnvVars, ownerUserId)
Child-->>Handler: ExecutionResult
Handler->>Handler: projectCustomBlockOutput(result, exposedOutputs, childCost)
Handler-->>Consumer: BlockOutput success + namedOutputs
Loading

Reviews (4): Last reviewed commit: "feat(custom-block): run child under sour..." | Re-trigger Greptile

Comment threadapps/sim/lib/workflows/custom-blocks/operations.ts
Comment threadapps/sim/lib/workflows/custom-blocks/operations.ts
@greptile-apps

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces the "deploy-as-block" feature, allowing org admins to publish a deployed workflow as a reusable, org-scoped custom block that appears in the block palette like any native block. It is a large but well-structured addition gated behind a feature flag and an enterprise plan check.

  • New custom_block table (migration 0254) with FK cascades to organization, workflow, and user; a composite unique index on (organization_id, type) ensures the generated type slug is unique per org.
  • Dual-environment block registry overlay: an AsyncLocalStorage-backed server overlay for request isolation and a useSyncExternalStore-backed client overlay hydrated by CustomBlocksLoader, giving every synchronous getBlock/getAllBlocks call site access to custom block configs without breaking either environment.
  • Invocation-boundary execution model: authority (bound workflow id + owner user id + exposed outputs) is always read from the DB at runtime, never from the serialized block, so cross-workspace consumers need no permission on the source workflow.

Confidence Score: 3/5

The core invocation-boundary model and registry overlay are solid, but two correctness issues in the client overlay and the DB schema need fixing before this ships to production.

The client overlay is hydrated with all blocks including disabled ones — an admin who disables a block to pull it from users palettes will find it still appears and is placeable (execution then fails with a confusing error). Separately, the DB has no unique constraint on (organization_id, workflow_id), so direct API calls can create multiple custom blocks for the same source workflow; the UI only surfaces the first match, leaving extras permanently orphaned in the DB but still resolvable at runtime.

custom-blocks-loader.tsx (disabled-block filter missing), packages/db/schema.ts and operations.ts (missing unique constraint + server-side validation on workflow_id per org), and operations.ts getCustomBlockAuthority (no index on type alone for the execution hot path).

Important Files Changed

FilenameOverview
apps/sim/app/workspace/[workspaceId]/providers/custom-blocks-loader.tsxHydrates the client overlay with ALL custom blocks including disabled ones — disabled blocks appear in the palette and can be placed on workflows.
packages/db/schema.tsNew custom_block table with correct FK cascades and org-type unique index, but missing a unique constraint on (organization_id, workflow_id) that would prevent duplicate blocks for the same workflow.
apps/sim/lib/workflows/custom-blocks/operations.tsCore CRUD and authority-resolution logic; getCustomBlockAuthority queries by type alone without a dedicated index, and publishCustomBlock lacks a server-side uniqueness check per workflow per org.
apps/sim/executor/handlers/workflow/workflow-handler.tsCustom block execution added via invocation-boundary pattern; authority loaded from DB, owner userId used for deployed-context load, and curated outputs projected correctly without leaking child internals.
apps/sim/blocks/custom/server-overlay.tsAsyncLocalStorage overlay correctly isolates per-org custom block resolution across concurrent requests; placeholder icon and schema-agnostic empty inputFields appropriately omit client-only concerns.
apps/sim/blocks/custom/build-config.tsWell-structured block config synthesis: stable field-id anchoring for rename-safety, correct sub-block type mapping, and clean output projection.
apps/sim/app/api/custom-blocks/route.tsGET returns all blocks (enabled + disabled) to any workspace member; admin-only vs member visibility of disabled blocks is not differentiated.
apps/sim/app/api/custom-blocks/[id]/route.tsPATCH and DELETE correctly gate on feature flag + org admin/owner; authorizeManage loads the block first to derive organizationId, preventing org spoofing.
apps/sim/lib/workflows/executor/execution-core.tsWraps execution in the custom-block overlay at the shared choke point; queries getCustomBlockRowsForWorkspace independently, duplicating the fetch already done in the execute route.
packages/db/migrations/0254_custom_block.sqlAdditive migration with correct FK + cascade semantics; no index on type alone for the execution-time authority lookup.
apps/sim/blocks/custom/client-overlay.tsExternal-store pattern for notifying useSyncExternalStore subscribers on overlay changes is correct; version bump on hydrate ensures Access Control block list refreshes without a page reload.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/block/block.tsxDeploy UI correctly derives output options from live workflow state and reconciles existing curated outputs; JSON.stringify comparison for dirty detection is order-sensitive but unlikely to cause problems in practice.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Admin as Org Admin
participant API as POST /api/custom-blocks
participant DB as custom_block table
participant Loader as CustomBlocksLoader
participant Overlay as Client Overlay
participant Canvas as Block Palette
participant Exec as WorkflowBlockHandler
participant Auth as getCustomBlockAuthority
participant Child as Child Workflow
Admin->>API: Publish workflow as block
API->>DB: INSERT custom_block
DB-->>API: row
API-->>Admin: customBlock
Loader->>API: GET /api/custom-blocks
API->>DB: SELECT all blocks for org
DB-->>API: all rows including disabled
API-->>Loader: customBlocks array
Loader->>Overlay: hydrateClientCustomBlocks all blocks
Overlay-->>Canvas: custom_block types resolve
Canvas->>Exec: execute custom block
Exec->>Auth: getCustomBlockAuthority by type
Auth->>DB: SELECT WHERE type equals value
DB-->>Auth: authority row
Auth-->>Exec: workflowId ownerUserId exposedOutputs
Exec->>Child: loadChildWorkflowDeployed
Child-->>Exec: deployed snapshot
Exec->>Child: execute
Child-->>Exec: ExecutionResult
Exec->>Exec: projectCustomBlockOutput
Exec-->>Canvas: BlockOutput curated only
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 Org Admin
participant API as POST /api/custom-blocks
participant DB as custom_block table
participant Loader as CustomBlocksLoader
participant Overlay as Client Overlay
participant Canvas as Block Palette
participant Exec as WorkflowBlockHandler
participant Auth as getCustomBlockAuthority
participant Child as Child Workflow
Admin->>API: Publish workflow as block
API->>DB: INSERT custom_block
DB-->>API: row
API-->>Admin: customBlock
Loader->>API: GET /api/custom-blocks
API->>DB: SELECT all blocks for org
DB-->>API: all rows including disabled
API-->>Loader: customBlocks array
Loader->>Overlay: hydrateClientCustomBlocks all blocks
Overlay-->>Canvas: custom_block types resolve
Canvas->>Exec: execute custom block
Exec->>Auth: getCustomBlockAuthority by type
Auth->>DB: SELECT WHERE type equals value
DB-->>Auth: authority row
Auth-->>Exec: workflowId ownerUserId exposedOutputs
Exec->>Child: loadChildWorkflowDeployed
Child-->>Exec: deployed snapshot
Exec->>Child: execute
Child-->>Exec: ExecutionResult
Exec->>Exec: projectCustomBlockOutput
Exec-->>Canvas: BlockOutput curated only
Loading

Reviews (2): Last reviewed commit: "feat(custom-block): deploy a workflow as..." | Re-trigger Greptile

Comment threadapps/sim/app/workspace/[workspaceId]/providers/custom-blocks-loader.tsx Outdated
Comment threadapps/sim/app/workspace/[workspaceId]/providers/custom-blocks-loader.tsx Outdated
Comment threadpackages/db/schema.ts
Comment threadapps/sim/lib/workflows/custom-blocks/operations.ts
Comment threadapps/sim/app/api/workflows/[id]/execute/route.ts
@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@greptile review

Comment threadapps/sim/executor/handlers/workflow/workflow-handler.ts
Comment threadapps/sim/app/api/custom-blocks/route.ts
Comment threadapps/sim/lib/workflows/custom-blocks/operations.ts
# Conflicts:
#	scripts/check-api-validation-contracts.ts
Comment threadapps/sim/executor/handlers/workflow/workflow-handler.ts
Comment threadapps/sim/lib/api/contracts/custom-blocks.ts
@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@greptile review

Comment threadapps/sim/lib/workflows/custom-blocks/operations.ts
Comment threadapps/sim/lib/workflows/custom-blocks/operations.ts
Comment threadapps/sim/lib/workflows/custom-blocks/operations.ts Outdated
Comment threadapps/sim/executor/handlers/workflow/workflow-handler.ts
Comment threadapps/sim/executor/handlers/workflow/workflow-handler.ts
Comment threadapps/sim/executor/handlers/workflow/workflow-handler.ts
Comment threadapps/sim/blocks/custom/server-overlay.ts Outdated

@cursorcursorBot 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 2 potential issues.

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 0b5da48. Configure here.

Comment threadapps/sim/lib/workflows/executor/execution-core.ts
Comment threadapps/sim/lib/copilot/vfs/workspace-vfs.ts
@TheodoreSpeaks
TheodoreSpeaks merged commit 82eff54 into stagingJul 4, 2026
17 checks passed
@TheodoreSpeaks
TheodoreSpeaks deleted the feat/custom-block branch July 4, 2026 19:57
Sign up for freeto 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

@TheodoreSpeaks