feat(custom-block): deploy a workflow as a reusable org-scoped block#5407
Conversation
PR SummaryHigh Risk Overview API & publish UX: Registry overlay: Client hydration ( Execution: Copilot: Org blocks materialize as VFS Reviewed by Cursor Bugbot for commit 3e1e453. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
@greptile review |
Greptile SummaryThis 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
Confidence Score: 5/5The 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
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
%%{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
Reviews (4): Last reviewed commit: "feat(custom-block): run child under sour..." | Re-trigger Greptile |
Greptile SummaryThis 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.
Confidence Score: 3/5The 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
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
%%{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
Reviews (2): Last reviewed commit: "feat(custom-block): deploy a workflow as..." | Re-trigger Greptile |
|
@greptile review |
# Conflicts: # scripts/check-api-validation-contracts.ts
…eep field ids, hide disabled
…inputs to the agent
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ 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.


Summary
deploy-as-blockfeature flag; org admins disable it via Access Control like any other blockedit_workflow/get_blocks_metadataregistry overlay (the typed VFS snapshot is untouched, so the Go diff is unaffected)custom_blocktable (migration 0254 — additive, backward-compatible)Type of Change
Testing
bun run lint,bun run check:api-validation:strict, andbun run check:migrations origin/stagingall passChecklist