feat(custom-block): deploy a workflow as a reusable org-scoped block by TheodoreSpeaks · Pull Request #5407 · simstudioai/sim · GitHub
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

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

@cursor

cursor Bot 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 thread apps/sim/executor/handlers/workflow/workflow-handler.ts
Comment thread apps/sim/lib/workflows/custom-blocks/operations.ts
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@greptile-apps

greptile-apps Bot 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

Filename Overview
apps/sim/lib/workflows/custom-blocks/operations.ts Core 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.ts Extends 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.ts Per-request AsyncLocalStorage overlay making custom_block_* types resolvable through the synchronous registry accessors. Clean, well-isolated design.
apps/sim/blocks/custom/build-config.ts Synthesizes BlockConfig from a DB row and live input fields. Uses stable field IDs as sub-block IDs for rename-safe wiring.
packages/db/schema.ts New 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.ts GET 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.ts PATCH/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.tsx Client-side hydrator. Correctly filters by enabled before calling hydrateClientCustomBlocks.
apps/sim/lib/workflows/executor/execution-core.ts Wraps 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.tsx BlockDeploy 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 thread apps/sim/lib/workflows/custom-blocks/operations.ts
Comment thread apps/sim/lib/workflows/custom-blocks/operations.ts
@greptile-apps

greptile-apps Bot commented Jul 4, 2026

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

Filename Overview
apps/sim/app/workspace/[workspaceId]/providers/custom-blocks-loader.tsx Hydrates 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.ts New 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.ts Core 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.ts Custom 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.ts AsyncLocalStorage 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.ts Well-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.ts GET 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.ts PATCH 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.ts Wraps 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.sql Additive migration with correct FK + cascade semantics; no index on type alone for the execution-time authority lookup.
apps/sim/blocks/custom/client-overlay.ts External-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.tsx Deploy 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 thread apps/sim/app/workspace/[workspaceId]/providers/custom-blocks-loader.tsx Outdated
Comment thread apps/sim/app/workspace/[workspaceId]/providers/custom-blocks-loader.tsx Outdated
Comment thread packages/db/schema.ts
Comment thread apps/sim/lib/workflows/custom-blocks/operations.ts
Comment thread apps/sim/app/api/workflows/[id]/execute/route.ts
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

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

Copy link
Copy Markdown
Collaborator Author

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

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