+
@@ -52,21 +60,29 @@ function CheckboxItem({ blockId, option, isPreview, subBlockValues, disabled }:
>
{option.label}
+ {option.description && (
+
+
+
+
+
+ {option.description}
+
+
+ )}
)
}
export function CheckboxList({
blockId,
- subBlockId,
- title,
options,
isPreview = false,
subBlockValues,
disabled = false,
}: CheckboxListProps) {
return (
-
+
{options.map((option) => (
>> = {
+ 'slack-setup-wizard': SlackSetupWizard,
+}
+
+export type ModalId = keyof typeof MODAL_REGISTRY
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/index.ts
new file mode 100644
index 00000000000..2caab15a639
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/index.ts
@@ -0,0 +1 @@
+export { SlackSetupWizard } from './slack-setup-wizard'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx
new file mode 100644
index 00000000000..f9ccbf6ed2e
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx
@@ -0,0 +1,573 @@
+'use client'
+
+import { type ReactNode, useCallback, useMemo, useState } from 'react'
+import { Check, ChevronRight, Clipboard, Info } from 'lucide-react'
+import { useShallow } from 'zustand/react/shallow'
+import { Checkbox, Input, Label, SecretInput, Tooltip, toast, Wizard } from '@/components/emcn'
+import { cn } from '@/lib/core/utils/cn'
+import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
+import { useWebhookManagement } from '@/hooks/use-webhook-management'
+import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
+import { useSubBlockStore } from '@/stores/workflows/subblock/store'
+import {
+ buildSlackManifest,
+ SLACK_CAPABILITIES,
+ type SlackCapability,
+ type SlackCapabilityGroup,
+} from '@/triggers/slack/capabilities'
+
+const DEFAULT_APP_NAME = 'Sim Workflow Bot'
+
+const GROUP_LABELS: Record = {
+ trigger: 'Triggers',
+ action: 'Actions',
+}
+
+const GROUP_ORDER: readonly SlackCapabilityGroup[] = ['trigger', 'action'] as const
+
+const MODAL_HEIGHT_CLASS = 'h-[580px]'
+
+interface SlackSetupWizardProps {
+ blockId: string
+ isPreview?: boolean
+ disabled?: boolean
+}
+
+/**
+ * Slack app setup wizard sub-block.
+ *
+ * @remarks
+ * The panel renders a single launcher button. The wizard lives in a modal
+ * with a fixed-height body so navigating between steps doesn't resize the
+ * dialog. Credentials are written directly into the sibling `signingSecret`
+ * and `botToken` sub-blocks via the shared sub-block store, so those fields
+ * in the panel are populated by the time the user clicks Done.
+ */
+export function SlackSetupWizard({
+ blockId,
+ isPreview = false,
+ disabled = false,
+}: SlackSetupWizardProps) {
+ const [open, setOpen] = useState(false)
+ const launcherDisabled = isPreview || disabled
+
+ return (
+ <>
+ setOpen(true)}
+ disabled={launcherDisabled}
+ className={cn(
+ 'flex w-full items-center justify-between rounded-md border border-[var(--border-muted)] bg-[var(--surface-1)] px-3 py-2 text-left transition-colors',
+ launcherDisabled
+ ? 'cursor-not-allowed opacity-70'
+ : 'cursor-pointer hover-hover:bg-[var(--surface-hover)]'
+ )}
+ >
+ Setup Slack App
+
+
+
+
+ >
+ )
+}
+
+interface WizardModalProps {
+ blockId: string
+ open: boolean
+ onOpenChange: (next: boolean) => void
+ isPreview: boolean
+ disabled: boolean
+}
+
+function WizardModal({ blockId, open, onOpenChange, isPreview, disabled }: WizardModalProps) {
+ const [step, setStep] = useState(0)
+
+ const { webhookUrl, isLoading } = useWebhookManagement({
+ blockId,
+ triggerId: 'slack_webhook',
+ useWebhookUrl: true,
+ isPreview,
+ })
+
+ const [appName, setAppName] = useSubBlockValue(blockId, 'botDisplayName')
+ const [signingSecret, setSigningSecret] = useSubBlockValue(blockId, 'signingSecret')
+ const [botToken, setBotToken] = useSubBlockValue(blockId, 'botToken')
+ const selected = useCapabilitySelection(blockId)
+
+ const displayAppName = appName ?? DEFAULT_APP_NAME
+ const effectiveWebhookUrl = !isLoading && webhookUrl ? webhookUrl : null
+ const canCopy = effectiveWebhookUrl !== null
+ const controlsDisabled = isPreview || disabled
+
+ const manifestJson = useMemo(() => {
+ const manifest = buildSlackManifest(selected, {
+ appName: displayAppName.trim() || DEFAULT_APP_NAME,
+ webhookUrl: effectiveWebhookUrl,
+ })
+ return JSON.stringify(manifest, null, 2)
+ }, [selected, displayAppName, effectiveWebhookUrl])
+
+ const handleOpenChange = useCallback(
+ (next: boolean) => {
+ if (!next) setStep(0)
+ onOpenChange(next)
+ },
+ [onOpenChange]
+ )
+
+ return (
+
+
+ {
+ if (!controlsDisabled) setAppName(v)
+ }}
+ selected={selected}
+ disabled={controlsDisabled}
+ />
+
+
+
+
+
+ {
+ if (!controlsDisabled) setSigningSecret(v)
+ }}
+ disabled={controlsDisabled}
+ />
+
+
+ {
+ if (!controlsDisabled) setBotToken(v)
+ }}
+ disabled={controlsDisabled}
+ />
+
+
+
+
+
+ )
+}
+
+interface SubStepListProps {
+ children: ReactNode
+}
+
+function SubStepList({ children }: SubStepListProps) {
+ return {children}
+}
+
+interface SubStepProps {
+ n: number
+ children: ReactNode
+}
+
+function SubStep({ n, children }: SubStepProps) {
+ return (
+
+
+ {n}
+
+ {children}
+
+ )
+}
+
+interface StepConfigureProps {
+ blockId: string
+ appName: string
+ onAppNameChange: (next: string) => void
+ selected: ReadonlySet
+ disabled: boolean
+}
+
+function StepConfigure({
+ blockId,
+ appName,
+ onAppNameChange,
+ selected,
+ disabled,
+}: StepConfigureProps) {
+ return (
+
+
+ Pick a name and choose what events should trigger your workflow and what actions your bot
+ can take.
+
+
+
+ Bot name
+
+ onAppNameChange(e.target.value)}
+ disabled={disabled}
+ placeholder={DEFAULT_APP_NAME}
+ className='h-9 text-sm'
+ />
+
+
+ {GROUP_ORDER.map((group) => {
+ const items = SLACK_CAPABILITIES.filter((c) => c.group === group)
+ if (items.length === 0) return null
+ return (
+
+ )
+ })}
+
+
+ )
+}
+
+interface StepCreateProps {
+ manifestJson: string
+ canCopy: boolean
+}
+
+function StepCreate({ manifestJson, canCopy }: StepCreateProps) {
+ const [copied, setCopied] = useState(false)
+
+ const handleCopy = useCallback(async () => {
+ if (!canCopy) return
+ try {
+ await navigator.clipboard.writeText(manifestJson)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ } catch {
+ toast.error("Couldn't copy manifest — copy it manually from the developer console.")
+ }
+ }, [canCopy, manifestJson])
+
+ return (
+
+
+
+ Copy your manifest:
+
+
+ {canCopy ? 'Click to copy manifest' : 'Deploy once to lock in the webhook URL'}
+
+ {canCopy &&
+ (copied ? (
+
+ ) : (
+
+ ))}
+
+
+
+ Open the{' '}
+
+ Slack Apps page
+
+ .
+
+
+ Click Create New App → From a manifest and pick your
+ workspace.
+
+
+ Paste your manifest, then click Next → Create .
+
+
+
+ )
+}
+
+interface StepSecretProps {
+ blockId: string
+ value: string
+ onChange: (next: string) => void
+ disabled: boolean
+}
+
+function StepSecret({ blockId, value, onChange, disabled }: StepSecretProps) {
+ return (
+
+
+
+ In your new Slack app, open Basic Information .
+
+
+ Find Signing Secret and click Show , then copy it.
+
+ Paste it into the field below.
+
+
+
+ )
+}
+
+interface StepTokenProps {
+ blockId: string
+ value: string
+ onChange: (next: string) => void
+ disabled: boolean
+}
+
+function StepToken({ blockId, value, onChange, disabled }: StepTokenProps) {
+ return (
+
+
+
+ In Slack, open Install App → Install to Workspace and
+ authorize.
+
+
+ Copy the Bot User OAuth Token (starts with xoxb-).
+
+ Paste it into the field below.
+
+
+
+ )
+}
+
+interface SecretFieldProps {
+ id: string
+ label: string
+ value: string
+ onChange: (next: string) => void
+ disabled: boolean
+ placeholder?: string
+}
+
+/**
+ * Label + SecretInput pair used by the signing-secret and bot-token wizard
+ * steps. The masked-on-blur behavior lives in the emcn `SecretInput`
+ * primitive; this wrapper just pins the label/input composition the wizard
+ * reuses twice.
+ */
+function SecretField({ id, label, value, onChange, disabled, placeholder }: SecretFieldProps) {
+ return (
+
+
+ {label}
+
+
+
+ )
+}
+
+interface StepDoneProps {
+ hasSigningSecret: boolean
+ hasBotToken: boolean
+}
+
+function StepDone({ hasSigningSecret, hasBotToken }: StepDoneProps) {
+ return (
+
+
+ Your Slack app is set up. Save the workflow and Slack will verify the webhook URL
+ automatically.
+
+
+
+
+
+
+
+
+ Click Done and save this workflow.
+
+
+ )
+}
+
+interface StatusRowProps {
+ label: string
+ ok: boolean
+}
+
+function StatusRow({ label, ok }: StatusRowProps) {
+ return (
+
+
+
+ {label}
+ {!ok && — not saved yet }
+
+
+ )
+}
+
+interface CapabilityGroupProps {
+ blockId: string
+ label: string
+ capabilities: readonly SlackCapability[]
+ selected: ReadonlySet
+ disabled: boolean
+}
+
+function CapabilityGroup({
+ blockId,
+ label,
+ capabilities,
+ selected,
+ disabled,
+}: CapabilityGroupProps) {
+ return (
+
+
+ {label}
+
+
+ {capabilities.map((c) => (
+
+ ))}
+
+
+ )
+}
+
+interface CapabilityRowProps {
+ blockId: string
+ capability: SlackCapability
+ checked: boolean
+ disabled: boolean
+}
+
+function CapabilityRow({ blockId, capability, checked, disabled }: CapabilityRowProps) {
+ const [, setValue] = useSubBlockValue(blockId, capability.id)
+ const id = `${blockId}-wizard-${capability.id}`
+
+ const handleChange = useCallback(
+ (next: boolean) => {
+ if (disabled) return
+ setValue(next)
+ },
+ [disabled, setValue]
+ )
+
+ return (
+
+
handleChange(v === true)}
+ disabled={disabled}
+ />
+
+ {capability.label}
+
+
+
+
+
+
+ {capability.description}
+
+
+
+ )
+}
+
+/**
+ * Builds the set of enabled capability ids by reading each capability's
+ * individual sub-block value in a single shallow-compared store selector.
+ * A `null`/`undefined` store value falls back to the capability's
+ * `defaultChecked` so untouched configs still reflect the defaults.
+ */
+function useCapabilitySelection(blockId: string): ReadonlySet {
+ const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
+ const enabledFlags = useSubBlockStore(
+ useShallow((state) => {
+ const blockValues = activeWorkflowId
+ ? state.workflowValues[activeWorkflowId]?.[blockId]
+ : undefined
+ return SLACK_CAPABILITIES.map((c) => {
+ const raw = blockValues?.[c.id]
+ return typeof raw === 'boolean' ? raw : c.defaultChecked
+ })
+ })
+ )
+ return useMemo(
+ () => new Set(SLACK_CAPABILITIES.filter((_, i) => enabledFlags[i]).map((c) => c.id)),
+ [enabledFlags]
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx
index d85adff7b7e..a354f177e08 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx
@@ -50,6 +50,7 @@ import {
VariablesInput,
WorkflowSelectorInput,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components'
+import { MODAL_REGISTRY } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/modal-registry'
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import type { SubBlockConfig } from '@/blocks/types'
import { useWebhookManagement } from '@/hooks/use-webhook-management'
@@ -815,9 +816,14 @@ function SubBlockComponent({
return (
)
+ case 'modal': {
+ const ModalComponent = config.modalId ? MODAL_REGISTRY[config.modalId] : undefined
+ if (!ModalComponent) {
+ return (
+
+ Unknown modal: {String(config.modalId)}
+
+ )
+ }
+ return
+ }
case 'messages-input':
return (
{
'filter-builder',
'sort-builder',
'skill-input',
+ 'modal',
]
const blocks = getAllBlocks()
diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts
index 0d1b204e03f..79242ff0037 100644
--- a/apps/sim/blocks/types.ts
+++ b/apps/sim/blocks/types.ts
@@ -162,6 +162,7 @@ export type SubBlockType =
| 'text' // Read-only text display
| 'router-input' // Router route definitions with descriptions
| 'table-selector' // Table selector with link to view table
+ | 'modal' // Launches a modal component resolved via the client-side modal registry
/**
* Selector types that require display name hydration
@@ -302,6 +303,8 @@ export interface SubBlockConfig {
icon?: React.ComponentType<{ className?: string }>
group?: string
hidden?: boolean
+ defaultChecked?: boolean
+ description?: string
}[]
| (() => {
label: string
@@ -309,6 +312,8 @@ export interface SubBlockConfig {
icon?: React.ComponentType<{ className?: string }>
group?: string
hidden?: boolean
+ defaultChecked?: boolean
+ description?: string
}[])
min?: number
max?: number
@@ -325,6 +330,7 @@ export interface SubBlockConfig {
hideWhenEnvSet?: string // Hide this subblock when the named NEXT_PUBLIC_ env var is truthy
description?: string
tooltip?: string // Tooltip text displayed via info icon next to the title
+ modalId?: string // Registry key when type is 'modal'; see sub-block/components/modal-registry.ts
value?: (params: Record) => string
grouped?: boolean
scrollable?: boolean
diff --git a/apps/sim/components/emcn/components/index.ts b/apps/sim/components/emcn/components/index.ts
index c001fbb77f5..70c7a6b383c 100644
--- a/apps/sim/components/emcn/components/index.ts
+++ b/apps/sim/components/emcn/components/index.ts
@@ -124,6 +124,7 @@ export {
SModalTabsTrigger,
SModalTrigger,
} from './s-modal/s-modal'
+export { SecretInput, type SecretInputProps } from './secret-input/secret-input'
export { Skeleton } from './skeleton/skeleton'
export { Slider, type SliderProps } from './slider/slider'
export { Switch } from './switch/switch'
@@ -159,3 +160,4 @@ export {
type TourTooltipPlacement,
type TourTooltipProps,
} from './tour-tooltip/tour-tooltip'
+export { Wizard, type WizardProps, type WizardStepProps } from './wizard/wizard'
diff --git a/apps/sim/components/emcn/components/secret-input/secret-input.tsx b/apps/sim/components/emcn/components/secret-input/secret-input.tsx
new file mode 100644
index 00000000000..923270e9983
--- /dev/null
+++ b/apps/sim/components/emcn/components/secret-input/secret-input.tsx
@@ -0,0 +1,69 @@
+/**
+ * A password-style input that masks its value with bullets only while the
+ * field is unfocused.
+ *
+ * @remarks
+ * Unlike a standard ` `, this keeps the real text
+ * visible while the user is actively editing — so they can verify pasted
+ * secrets like signing tokens or API keys — and only swaps to bullets on
+ * blur. Uses plain `type="text"` so password managers don't auto-fill.
+ *
+ * @example
+ * ```tsx
+ * import { SecretInput } from '@/components/emcn'
+ *
+ *
+ * ```
+ */
+'use client'
+
+import * as React from 'react'
+import { Input, type InputProps } from '../input/input'
+
+export interface SecretInputProps
+ extends Omit {
+ /** Current value. Rendered as bullets when the input is not focused. */
+ value: string
+ /** Called with the new value on every real edit (focused-only). */
+ onChange: (next: string) => void
+}
+
+const SecretInput = React.forwardRef(
+ ({ value, onChange, onFocus, onBlur, ...props }, ref) => {
+ const [isFocused, setIsFocused] = React.useState(false)
+ const displayValue = isFocused ? value : '•'.repeat(value.length)
+
+ return (
+ {
+ // Guard against synthetic change events (autofill, form reset)
+ // firing while blurred, which would otherwise overwrite the real
+ // value with bullet characters.
+ if (!isFocused) return
+ onChange(e.target.value)
+ }}
+ onFocus={(e) => {
+ setIsFocused(true)
+ onFocus?.(e)
+ }}
+ onBlur={(e) => {
+ setIsFocused(false)
+ onBlur?.(e)
+ }}
+ autoComplete='off'
+ {...props}
+ />
+ )
+ }
+)
+SecretInput.displayName = 'SecretInput'
+
+export { SecretInput }
diff --git a/apps/sim/components/emcn/components/wizard/wizard.tsx b/apps/sim/components/emcn/components/wizard/wizard.tsx
new file mode 100644
index 00000000000..ee68ee3742a
--- /dev/null
+++ b/apps/sim/components/emcn/components/wizard/wizard.tsx
@@ -0,0 +1,194 @@
+'use client'
+
+import * as React from 'react'
+import { cn } from '@/lib/core/utils/cn'
+import { Button } from '../button/button'
+import {
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ type ModalSize,
+} from '../modal/modal'
+
+/**
+ * A multi-step modal wizard primitive.
+ *
+ * @remarks
+ * Wraps the emcn Modal with a step progress bar, a shared Back / Next / Done
+ * footer, and declarative `Wizard.Step` children. Step state is controlled
+ * from the outside so the consumer can hydrate from persisted state, reset
+ * on close, or jump around imperatively.
+ *
+ * @example
+ * ```tsx
+ * const [open, setOpen] = useState(false)
+ * const [step, setStep] = useState(0)
+ *
+ * { if (!next) setStep(0); setOpen(next) }}
+ * currentStep={step}
+ * onStepChange={setStep}
+ * size='lg'
+ * height='h-[580px]'
+ * >
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ */
+
+export interface WizardStepProps {
+ /** Title shown in the modal header when this step is active. */
+ title: string
+ /** Step body. Rendered inside the modal body when this step is active. */
+ children: React.ReactNode
+ /**
+ * When false, the Next button on this step is disabled. Lets the consumer
+ * gate progression on validation or async state.
+ * @default true
+ */
+ canAdvance?: boolean
+}
+
+/**
+ * Declares one step in the wizard. Carries metadata via props; the actual
+ * children are extracted and rendered by the `Wizard` root.
+ */
+const Step: React.FC = ({ children }) => <>{children}>
+Step.displayName = 'Wizard.Step'
+
+const STEP_DISPLAY_NAME = 'Wizard.Step'
+
+function isStepElement(node: React.ReactNode): node is React.ReactElement {
+ if (!React.isValidElement(node)) return false
+ const type = node.type as { displayName?: string } | string
+ return typeof type !== 'string' && type?.displayName === STEP_DISPLAY_NAME
+}
+
+export interface WizardProps {
+ /** Whether the wizard modal is open. */
+ open: boolean
+ /** Called when the modal's open state changes. */
+ onOpenChange: (next: boolean) => void
+ /** Zero-indexed current step. */
+ currentStep: number
+ /** Called with the new step index when the user clicks Back / Next. */
+ onStepChange: (next: number) => void
+ /**
+ * Modal size variant. Matches `ModalContent` sizes.
+ * @default 'lg'
+ */
+ size?: ModalSize
+ /**
+ * Optional fixed height for the modal content. Pass a Tailwind class
+ * (e.g. `h-[580px]`) to keep the modal a stable size across steps.
+ */
+ height?: string
+ /**
+ * Called when the user clicks Done on the final step. Fires before the
+ * modal closes; the wizard will close the modal itself after.
+ */
+ onComplete?: () => void
+ /** One or more `` elements. Non-step children are ignored. */
+ children: React.ReactNode
+ /** Label for the Back button. @default 'Back' */
+ backLabel?: string
+ /** Label for the Next button. @default 'Next' */
+ nextLabel?: string
+ /** Label for the Done button on the final step. @default 'Done' */
+ doneLabel?: string
+}
+
+const WizardRoot: React.FC = ({
+ open,
+ onOpenChange,
+ currentStep,
+ onStepChange,
+ size = 'lg',
+ height,
+ onComplete,
+ children,
+ backLabel = 'Back',
+ nextLabel = 'Next',
+ doneLabel = 'Done',
+}) => {
+ const steps = React.Children.toArray(children).filter(isStepElement)
+ const total = steps.length
+ const clamped = Math.min(Math.max(0, currentStep), Math.max(0, total - 1))
+ const activeStep = steps[clamped]
+ const canAdvance = activeStep?.props.canAdvance ?? true
+ const isLast = total > 0 && clamped === total - 1
+
+ const handleBack = React.useCallback(() => {
+ onStepChange(Math.max(0, clamped - 1))
+ }, [clamped, onStepChange])
+
+ const handleNext = React.useCallback(() => {
+ onStepChange(Math.min(total - 1, clamped + 1))
+ }, [clamped, total, onStepChange])
+
+ const handleDone = React.useCallback(() => {
+ onComplete?.()
+ onOpenChange(false)
+ }, [onComplete, onOpenChange])
+
+ if (total === 0) return null
+
+ return (
+
+
+
+
+ {activeStep?.props.title}
+
+ Step {clamped + 1} of {total}
+
+
+
+
+
+ {steps.map((_step, i) => (
+
+ ))}
+
+
+ {activeStep}
+
+
+
+ {backLabel}
+
+ {isLast ? (
+
+ {doneLabel}
+
+ ) : (
+
+ {nextLabel}
+
+ )}
+
+
+
+ )
+}
+
+export const Wizard = Object.assign(WizardRoot, {
+ Step,
+})
diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts
index 95538651008..8a64b57a766 100644
--- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts
+++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts
@@ -10,7 +10,7 @@ export interface ToolRuntimeSchemaEntry {
}
export const TOOL_RUNTIME_SCHEMAS: Record = {
- ['agent']: {
+ agent: {
parameters: {
properties: {
request: {
@@ -23,7 +23,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['auth']: {
+ auth: {
parameters: {
properties: {
request: {
@@ -36,7 +36,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['check_deployment_status']: {
+ check_deployment_status: {
parameters: {
type: 'object',
properties: {
@@ -48,7 +48,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['complete_job']: {
+ complete_job: {
parameters: {
type: 'object',
properties: {
@@ -61,7 +61,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['context_write']: {
+ context_write: {
parameters: {
type: 'object',
properties: {
@@ -78,7 +78,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['crawl_website']: {
+ crawl_website: {
parameters: {
type: 'object',
properties: {
@@ -113,7 +113,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['create_file']: {
+ create_file: {
parameters: {
type: 'object',
properties: {
@@ -149,7 +149,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
required: ['success', 'message'],
},
},
- ['create_folder']: {
+ create_folder: {
parameters: {
type: 'object',
properties: {
@@ -170,7 +170,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['create_job']: {
+ create_job: {
parameters: {
type: 'object',
properties: {
@@ -220,7 +220,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['create_workflow']: {
+ create_workflow: {
parameters: {
type: 'object',
properties: {
@@ -245,7 +245,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['create_workspace_mcp_server']: {
+ create_workspace_mcp_server: {
parameters: {
type: 'object',
properties: {
@@ -266,7 +266,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['debug']: {
+ debug: {
parameters: {
properties: {
context: {
@@ -285,7 +285,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['delete_file']: {
+ delete_file: {
parameters: {
type: 'object',
properties: {
@@ -314,7 +314,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
required: ['success', 'message'],
},
},
- ['delete_folder']: {
+ delete_folder: {
parameters: {
type: 'object',
properties: {
@@ -330,7 +330,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['delete_workflow']: {
+ delete_workflow: {
parameters: {
type: 'object',
properties: {
@@ -346,7 +346,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['delete_workspace_mcp_server']: {
+ delete_workspace_mcp_server: {
parameters: {
type: 'object',
properties: {
@@ -359,7 +359,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['deploy']: {
+ deploy: {
parameters: {
properties: {
request: {
@@ -373,7 +373,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['deploy_api']: {
+ deploy_api: {
parameters: {
type: 'object',
properties: {
@@ -447,7 +447,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
],
},
},
- ['deploy_chat']: {
+ deploy_chat: {
parameters: {
type: 'object',
properties: {
@@ -595,7 +595,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
],
},
},
- ['deploy_mcp']: {
+ deploy_mcp: {
parameters: {
type: 'object',
properties: {
@@ -711,7 +711,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
required: ['deploymentType', 'deploymentStatus'],
},
},
- ['download_to_workspace_file']: {
+ download_to_workspace_file: {
parameters: {
type: 'object',
properties: {
@@ -730,7 +730,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['edit_content']: {
+ edit_content: {
parameters: {
type: 'object',
properties: {
@@ -762,7 +762,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
required: ['success', 'message'],
},
},
- ['edit_workflow']: {
+ edit_workflow: {
parameters: {
type: 'object',
properties: {
@@ -801,13 +801,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['file']: {
+ file: {
parameters: {
type: 'object',
},
resultSchema: undefined,
},
- ['function_execute']: {
+ function_execute: {
parameters: {
type: 'object',
properties: {
@@ -868,7 +868,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['generate_api_key']: {
+ generate_api_key: {
parameters: {
type: 'object',
properties: {
@@ -886,7 +886,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['generate_image']: {
+ generate_image: {
parameters: {
type: 'object',
properties: {
@@ -923,7 +923,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['generate_visualization']: {
+ generate_visualization: {
parameters: {
type: 'object',
properties: {
@@ -963,7 +963,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['get_block_outputs']: {
+ get_block_outputs: {
parameters: {
type: 'object',
properties: {
@@ -984,7 +984,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['get_block_upstream_references']: {
+ get_block_upstream_references: {
parameters: {
type: 'object',
properties: {
@@ -1006,7 +1006,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['get_deployed_workflow_state']: {
+ get_deployed_workflow_state: {
parameters: {
type: 'object',
properties: {
@@ -1019,7 +1019,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['get_deployment_version']: {
+ get_deployment_version: {
parameters: {
type: 'object',
properties: {
@@ -1036,7 +1036,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['get_execution_summary']: {
+ get_execution_summary: {
parameters: {
type: 'object',
properties: {
@@ -1063,7 +1063,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['get_job_logs']: {
+ get_job_logs: {
parameters: {
type: 'object',
properties: {
@@ -1088,7 +1088,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['get_page_contents']: {
+ get_page_contents: {
parameters: {
type: 'object',
properties: {
@@ -1116,14 +1116,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['get_platform_actions']: {
+ get_platform_actions: {
parameters: {
type: 'object',
properties: {},
},
resultSchema: undefined,
},
- ['get_workflow_data']: {
+ get_workflow_data: {
parameters: {
type: 'object',
properties: {
@@ -1142,7 +1142,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['get_workflow_logs']: {
+ get_workflow_logs: {
parameters: {
type: 'object',
properties: {
@@ -1168,7 +1168,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['glob']: {
+ glob: {
parameters: {
type: 'object',
properties: {
@@ -1187,7 +1187,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['grep']: {
+ grep: {
parameters: {
type: 'object',
properties: {
@@ -1234,7 +1234,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['job']: {
+ job: {
parameters: {
properties: {
request: {
@@ -1247,7 +1247,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['knowledge']: {
+ knowledge: {
parameters: {
properties: {
request: {
@@ -1260,7 +1260,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['knowledge_base']: {
+ knowledge_base: {
parameters: {
type: 'object',
properties: {
@@ -1452,7 +1452,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
required: ['success', 'message'],
},
},
- ['list_folders']: {
+ list_folders: {
parameters: {
type: 'object',
properties: {
@@ -1464,14 +1464,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['list_user_workspaces']: {
+ list_user_workspaces: {
parameters: {
type: 'object',
properties: {},
},
resultSchema: undefined,
},
- ['list_workspace_mcp_servers']: {
+ list_workspace_mcp_servers: {
parameters: {
type: 'object',
properties: {
@@ -1483,7 +1483,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['manage_credential']: {
+ manage_credential: {
parameters: {
type: 'object',
properties: {
@@ -1512,7 +1512,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['manage_custom_tool']: {
+ manage_custom_tool: {
parameters: {
type: 'object',
properties: {
@@ -1591,7 +1591,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['manage_job']: {
+ manage_job: {
parameters: {
type: 'object',
properties: {
@@ -1661,7 +1661,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['manage_mcp_tool']: {
+ manage_mcp_tool: {
parameters: {
type: 'object',
properties: {
@@ -1712,7 +1712,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['manage_skill']: {
+ manage_skill: {
parameters: {
type: 'object',
properties: {
@@ -1744,7 +1744,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['materialize_file']: {
+ materialize_file: {
parameters: {
type: 'object',
properties: {
@@ -1778,7 +1778,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['move_folder']: {
+ move_folder: {
parameters: {
type: 'object',
properties: {
@@ -1796,7 +1796,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['move_workflow']: {
+ move_workflow: {
parameters: {
type: 'object',
properties: {
@@ -1816,7 +1816,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['oauth_get_auth_link']: {
+ oauth_get_auth_link: {
parameters: {
type: 'object',
properties: {
@@ -1830,7 +1830,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['oauth_request_access']: {
+ oauth_request_access: {
parameters: {
type: 'object',
properties: {
@@ -1844,7 +1844,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['open_resource']: {
+ open_resource: {
parameters: {
type: 'object',
properties: {
@@ -1872,7 +1872,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['read']: {
+ read: {
parameters: {
type: 'object',
properties: {
@@ -1899,7 +1899,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['redeploy']: {
+ redeploy: {
parameters: {
type: 'object',
properties: {
@@ -1967,7 +1967,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
],
},
},
- ['rename_file']: {
+ rename_file: {
parameters: {
type: 'object',
properties: {
@@ -2002,7 +2002,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
required: ['success', 'message'],
},
},
- ['rename_workflow']: {
+ rename_workflow: {
parameters: {
type: 'object',
properties: {
@@ -2019,7 +2019,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['research']: {
+ research: {
parameters: {
properties: {
topic: {
@@ -2032,7 +2032,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['respond']: {
+ respond: {
parameters: {
additionalProperties: true,
properties: {
@@ -2055,7 +2055,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['restore_resource']: {
+ restore_resource: {
parameters: {
type: 'object',
properties: {
@@ -2073,7 +2073,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['revert_to_version']: {
+ revert_to_version: {
parameters: {
type: 'object',
properties: {
@@ -2090,7 +2090,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['run']: {
+ run: {
parameters: {
properties: {
context: {
@@ -2107,7 +2107,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['run_block']: {
+ run_block: {
parameters: {
type: 'object',
properties: {
@@ -2139,7 +2139,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['run_from_block']: {
+ run_from_block: {
parameters: {
type: 'object',
properties: {
@@ -2171,7 +2171,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['run_workflow']: {
+ run_workflow: {
parameters: {
type: 'object',
properties: {
@@ -2199,7 +2199,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['run_workflow_until_block']: {
+ run_workflow_until_block: {
parameters: {
type: 'object',
properties: {
@@ -2231,7 +2231,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['scrape_page']: {
+ scrape_page: {
parameters: {
type: 'object',
properties: {
@@ -2252,7 +2252,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['search_documentation']: {
+ search_documentation: {
parameters: {
type: 'object',
properties: {
@@ -2269,7 +2269,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['search_library_docs']: {
+ search_library_docs: {
parameters: {
type: 'object',
properties: {
@@ -2290,7 +2290,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['search_online']: {
+ search_online: {
parameters: {
type: 'object',
properties: {
@@ -2331,7 +2331,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['search_patterns']: {
+ search_patterns: {
parameters: {
type: 'object',
properties: {
@@ -2353,7 +2353,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['set_block_enabled']: {
+ set_block_enabled: {
parameters: {
type: 'object',
properties: {
@@ -2375,7 +2375,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['set_environment_variables']: {
+ set_environment_variables: {
parameters: {
type: 'object',
properties: {
@@ -2409,7 +2409,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['set_global_workflow_variables']: {
+ set_global_workflow_variables: {
parameters: {
type: 'object',
properties: {
@@ -2447,7 +2447,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['superagent']: {
+ superagent: {
parameters: {
properties: {
task: {
@@ -2461,7 +2461,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['table']: {
+ table: {
parameters: {
properties: {
request: {
@@ -2474,7 +2474,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['tool_search_tool_regex']: {
+ tool_search_tool_regex: {
parameters: {
properties: {
case_insensitive: {
@@ -2495,7 +2495,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['update_job_history']: {
+ update_job_history: {
parameters: {
type: 'object',
properties: {
@@ -2513,7 +2513,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['update_workspace_mcp_server']: {
+ update_workspace_mcp_server: {
parameters: {
type: 'object',
properties: {
@@ -2538,7 +2538,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['user_memory']: {
+ user_memory: {
parameters: {
type: 'object',
properties: {
@@ -2586,7 +2586,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
- ['user_table']: {
+ user_table: {
parameters: {
type: 'object',
properties: {
@@ -2777,13 +2777,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
required: ['success', 'message'],
},
},
- ['workflow']: {
+ workflow: {
parameters: {
type: 'object',
},
resultSchema: undefined,
},
- ['workspace_file']: {
+ workspace_file: {
parameters: {
type: 'object',
properties: {
diff --git a/apps/sim/triggers/slack/capabilities.ts b/apps/sim/triggers/slack/capabilities.ts
new file mode 100644
index 00000000000..1047b2f92dd
--- /dev/null
+++ b/apps/sim/triggers/slack/capabilities.ts
@@ -0,0 +1,168 @@
+/**
+ * Slack app capabilities that can be toggled on in the manifest generator.
+ *
+ * @remarks
+ * Each capability maps to a set of bot OAuth scopes and bot events that must
+ * be declared in the Slack app manifest for the capability to work. The `id`
+ * is also used as the sub-block storage key (shape: `trigger_*` / `action_*`)
+ * so the same object serves as both a checkbox-list option and a manifest
+ * builder entry. See https://api.slack.com/reference/manifests.
+ */
+
+export type SlackCapabilityGroup = 'trigger' | 'action'
+
+export interface SlackCapability {
+ id: string
+ label: string
+ description: string
+ defaultChecked: boolean
+ group: SlackCapabilityGroup
+ scopes: readonly string[]
+ events: readonly string[]
+}
+
+export const SLACK_CAPABILITIES: readonly SlackCapability[] = [
+ {
+ id: 'trigger_mention',
+ label: '@mention',
+ description: 'Trigger the workflow when someone @-mentions your bot.',
+ defaultChecked: true,
+ group: 'trigger',
+ scopes: ['app_mentions:read'],
+ events: ['app_mention'],
+ },
+ {
+ id: 'trigger_dm',
+ label: 'Direct message',
+ description: 'Trigger the workflow when a user sends your bot a 1:1 direct message.',
+ defaultChecked: true,
+ group: 'trigger',
+ scopes: ['im:history', 'im:read'],
+ events: ['message.im'],
+ },
+ {
+ id: 'trigger_group_dm',
+ label: 'Group direct message',
+ description: 'Trigger on messages in multi-person DMs your bot is part of.',
+ defaultChecked: true,
+ group: 'trigger',
+ scopes: ['mpim:history', 'mpim:read'],
+ events: ['message.mpim'],
+ },
+ {
+ id: 'trigger_public_channel',
+ label: 'Public channel message',
+ description: 'Trigger on messages in public channels your bot has been invited to.',
+ defaultChecked: true,
+ group: 'trigger',
+ scopes: ['channels:history', 'channels:read'],
+ events: ['message.channels'],
+ },
+ {
+ id: 'trigger_private_channel',
+ label: 'Private channel message',
+ description: 'Trigger on messages in private channels your bot has been invited to.',
+ defaultChecked: true,
+ group: 'trigger',
+ scopes: ['groups:history', 'groups:read'],
+ events: ['message.groups'],
+ },
+ {
+ id: 'trigger_reaction',
+ label: 'Reaction',
+ description:
+ 'Trigger when an emoji reaction is added or removed anywhere the bot can see — public, private, or DM. Slack does not allow restricting the reactions scope by channel type.',
+ defaultChecked: true,
+ group: 'trigger',
+ scopes: ['reactions:read'],
+ events: ['reaction_added', 'reaction_removed'],
+ },
+ {
+ id: 'action_send',
+ label: 'Send messages',
+ description: 'Let the bot post messages into channels it is a member of.',
+ defaultChecked: true,
+ group: 'action',
+ scopes: ['chat:write'],
+ events: [],
+ },
+ {
+ id: 'action_add_reaction',
+ label: 'Add reactions',
+ description: 'Let the bot add emoji reactions to messages.',
+ defaultChecked: true,
+ group: 'action',
+ scopes: ['reactions:write'],
+ events: [],
+ },
+ {
+ id: 'action_read_files',
+ label: 'Read file attachments',
+ description: 'Let the bot download file attachments on incoming messages.',
+ defaultChecked: true,
+ group: 'action',
+ scopes: ['files:read'],
+ events: [],
+ },
+ {
+ id: 'action_read_users',
+ label: 'Look up users',
+ description: 'Resolve user IDs to names, profiles, and email addresses.',
+ defaultChecked: true,
+ group: 'action',
+ scopes: ['users:read', 'users:read.email'],
+ events: [],
+ },
+] as const
+
+const WEBHOOK_URL_PLACEHOLDER = ''
+
+export interface BuildManifestOptions {
+ appName: string
+ webhookUrl: string | null
+}
+
+/**
+ * Builds a Slack app manifest object from a set of enabled capability ids.
+ *
+ * @remarks
+ * - Deduplicates scopes and events across overlapping capabilities.
+ * - Omits `settings.event_subscriptions` entirely when no events are selected —
+ * Slack's manifest validator rejects an empty `bot_events` array.
+ * - When `webhookUrl` is null, embeds a human-readable placeholder so the
+ * shape is visible before the workflow is deployed.
+ */
+export function buildSlackManifest(
+ enabled: ReadonlySet,
+ { appName, webhookUrl }: BuildManifestOptions
+): Record {
+ const active = SLACK_CAPABILITIES.filter((c) => enabled.has(c.id))
+ const scopes = [...new Set(active.flatMap((c) => c.scopes))].sort()
+ const events = [...new Set(active.flatMap((c) => c.events))].sort()
+ const displayName = appName.trim() || 'Sim Workflow Bot'
+
+ const manifest: Record = {
+ display_information: { name: displayName },
+ features: {
+ bot_user: { display_name: displayName, always_online: true },
+ },
+ oauth_config: {
+ scopes: { bot: scopes },
+ },
+ settings: {
+ org_deploy_enabled: false,
+ socket_mode_enabled: false,
+ token_rotation_enabled: false,
+ },
+ }
+
+ if (events.length > 0) {
+ const settings = manifest.settings as Record
+ settings.event_subscriptions = {
+ request_url: webhookUrl ?? WEBHOOK_URL_PLACEHOLDER,
+ bot_events: events,
+ }
+ }
+
+ return manifest
+}
diff --git a/apps/sim/triggers/slack/webhook.ts b/apps/sim/triggers/slack/webhook.ts
index 72037b6bd82..c0d136aadbd 100644
--- a/apps/sim/triggers/slack/webhook.ts
+++ b/apps/sim/triggers/slack/webhook.ts
@@ -52,24 +52,11 @@ export const slackWebhookTrigger: TriggerConfig = {
mode: 'trigger',
},
{
- id: 'triggerInstructions',
- title: 'Setup Instructions',
- type: 'text',
- defaultValue: [
- 'Go to Slack Apps page ',
- 'If you don\'t have an app:Create an app from scratch Give it a name and select your workspace ',
- 'Go to "Basic Information", find the "Signing Secret", and paste it in the field above.',
- 'Go to "OAuth & Permissions" and add bot token scopes:app_mentions:read - For viewing messages that tag your bot with an @chat:write - To send messages to channels your bot is a part offiles:read - To access files and images shared in messagesreactions:read - For listening to emoji reactions and fetching reacted-to message text ',
- 'Go to "Event Subscriptions":Enable events Under "Subscribe to Bot Events", add app_mention to listen to messages that mention your bot To receive all channel messages, add message.channels. For DMs add message.im, for group DMs add message.mpim, for private channels add message.groups For reaction events, also add reaction_added and/or reaction_removed Paste the Webhook URL above into the "Request URL" field ',
- 'Go to "Install App" in the left sidebar and install the app into your desired Slack workspace and channel.',
- 'Copy the "Bot User OAuth Token" (starts with xoxb-) and paste it in the Bot Token field above to enable file downloads.',
- 'Save changes in both Slack and here.',
- ]
- .map(
- (instruction, index) =>
- `${index + 1}. ${instruction}
`
- )
- .join(''),
+ id: 'setupWizard',
+ title: 'Slack app setup',
+ type: 'modal',
+ modalId: 'slack-setup-wizard',
+ description: 'Walk through manifest creation, app install, and pasting credentials.',
hideFromPreview: true,
mode: 'trigger',
},