Skip to content

Commit 0c51d44

Browse files
committed
added wand for custom cron, fixed slack inconsistency
1 parent 529ff71 commit 0c51d44

10 files changed

Lines changed: 161 additions & 86 deletions

File tree

apps/docs/content/docs/en/tools/google_books.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
77

88
<BlockInfoCard
99
type="google_books"
10-
color="#FFFFFF"
10+
color="#E0E0E0"
1111
/>
1212

1313
## Usage Instructions

apps/docs/content/docs/en/tools/s3.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ Retrieve an object from an AWS S3 bucket
7171
| --------- | ---- | -------- | ----------- |
7272
| `accessKeyId` | string | Yes | Your AWS Access Key ID |
7373
| `secretAccessKey` | string | Yes | Your AWS Secret Access Key |
74+
| `region` | string | No | Optional region override when URL does not include region \(e.g., us-east-1, eu-west-1\) |
7475
| `s3Uri` | string | Yes | S3 Object URL \(e.g., https://bucket.s3.region.amazonaws.com/path/to/file\) |
7576

7677
#### Output

apps/docs/content/docs/en/tools/slack.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format
7979
| `channel` | string | No | Slack channel ID \(e.g., C1234567890\) |
8080
| `dmUserId` | string | No | Slack user ID for direct messages \(e.g., U1234567890\) |
8181
| `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) |
82-
| `thread_ts` | string | No | Thread timestamp to reply to \(creates thread reply\) |
82+
| `threadTs` | string | No | Thread timestamp to reply to \(creates thread reply\) |
8383
| `files` | file[] | No | Files to attach to the message |
8484

8585
#### Output

apps/sim/app/api/wand/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,11 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
238238
finalSystemPrompt += currentTimeContext
239239
}
240240

241+
if (generationType === 'cron-expression') {
242+
finalSystemPrompt +=
243+
'\n\nIMPORTANT: Return ONLY the raw cron expression (e.g., "0 9 * * 1-5"). Do NOT wrap it in markdown code blocks, backticks, or quotes. Do NOT include any explanation or text before or after the expression.'
244+
}
245+
241246
if (generationType === 'json-object') {
242247
finalSystemPrompt +=
243248
'\n\nIMPORTANT: Return ONLY the raw JSON object. Do NOT wrap it in markdown code blocks (no ```json or ```). Do NOT include any explanation or text before or after the JSON. The response must start with { and end with }.'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx

Lines changed: 127 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,36 +1741,97 @@ export const ToolInput = memo(function ToolInput({
17411741
) : null
17421742
})()}
17431743

1744-
{requiresOAuth && oauthConfig && (
1745-
<div className='relative min-w-0 space-y-[6px]'>
1746-
<div className='font-medium text-[13px] text-[var(--text-primary)]'>
1747-
Account <span className='ml-0.5'>*</span>
1748-
</div>
1749-
<div className='w-full min-w-0'>
1750-
<ToolCredentialSelector
1751-
value={tool.params?.credential || ''}
1752-
onChange={(value: string) =>
1753-
handleParamChange(toolIndex, 'credential', value)
1754-
}
1755-
provider={oauthConfig.provider as OAuthProvider}
1756-
requiredScopes={
1757-
toolBlock?.subBlocks?.find((sb) => sb.id === 'credential')
1758-
?.requiredScopes ||
1759-
getCanonicalScopesForProvider(oauthConfig.provider)
1760-
}
1761-
serviceId={oauthConfig.provider}
1762-
disabled={disabled}
1763-
/>
1764-
</div>
1765-
</div>
1766-
)}
1767-
17681744
{(() => {
17691745
const renderedElements: React.ReactNode[] = []
17701746

1747+
const showOAuth =
1748+
requiresOAuth && oauthConfig && tool.params?.authMethod !== 'bot_token'
1749+
1750+
const renderOAuthAccount = (): React.ReactNode => {
1751+
if (!showOAuth || !oauthConfig) return null
1752+
const credentialSubBlock = toolBlock?.subBlocks?.find(
1753+
(s) => s.type === 'oauth-input'
1754+
)
1755+
return (
1756+
<div key='oauth-account' className='relative min-w-0 space-y-[6px]'>
1757+
<div className='font-medium text-[13px] text-[var(--text-primary)]'>
1758+
{credentialSubBlock?.title || 'Account'}{' '}
1759+
<span className='ml-0.5'>*</span>
1760+
</div>
1761+
<div className='w-full min-w-0'>
1762+
<ToolCredentialSelector
1763+
value={tool.params?.credential || ''}
1764+
onChange={(value: string) =>
1765+
handleParamChange(toolIndex, 'credential', value)
1766+
}
1767+
provider={oauthConfig.provider as OAuthProvider}
1768+
requiredScopes={
1769+
credentialSubBlock?.requiredScopes ||
1770+
getCanonicalScopesForProvider(oauthConfig.provider)
1771+
}
1772+
serviceId={oauthConfig.provider}
1773+
disabled={disabled}
1774+
/>
1775+
</div>
1776+
</div>
1777+
)
1778+
}
1779+
1780+
const renderSubBlock = (sb: BlockSubBlockConfig): React.ReactNode => {
1781+
const effectiveParamId = sb.id
1782+
const canonicalId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id]
1783+
const canonicalGroup = canonicalId
1784+
? toolCanonicalIndex?.groupsById[canonicalId]
1785+
: undefined
1786+
const hasCanonicalPair = isCanonicalPair(canonicalGroup)
1787+
const canonicalMode =
1788+
canonicalGroup && hasCanonicalPair
1789+
? resolveCanonicalMode(
1790+
canonicalGroup,
1791+
{ operation: tool.operation, ...tool.params },
1792+
toolScopedOverrides
1793+
)
1794+
: undefined
1795+
1796+
const canonicalToggleProp =
1797+
hasCanonicalPair && canonicalMode && canonicalId
1798+
? {
1799+
mode: canonicalMode,
1800+
onToggle: () => {
1801+
const nextMode = canonicalMode === 'advanced' ? 'basic' : 'advanced'
1802+
collaborativeSetBlockCanonicalMode(
1803+
blockId,
1804+
`${tool.type}:${canonicalId}`,
1805+
nextMode
1806+
)
1807+
},
1808+
}
1809+
: undefined
1810+
1811+
const sbWithTitle = sb.title
1812+
? sb
1813+
: { ...sb, title: formatParameterLabel(effectiveParamId) }
1814+
1815+
return (
1816+
<ToolSubBlockRenderer
1817+
key={sb.id}
1818+
blockId={blockId}
1819+
subBlockId={subBlockId}
1820+
toolIndex={toolIndex}
1821+
subBlock={sbWithTitle}
1822+
effectiveParamId={effectiveParamId}
1823+
toolParams={tool.params}
1824+
onParamChange={handleParamChange}
1825+
disabled={disabled}
1826+
canonicalToggle={canonicalToggleProp}
1827+
/>
1828+
)
1829+
}
1830+
17711831
if (useSubBlocks && displaySubBlocks.length > 0) {
1832+
const allBlockSubBlocks = toolBlock?.subBlocks || []
17721833
const coveredParamIds = new Set(
1773-
displaySubBlocks.flatMap((sb) => {
1834+
allBlockSubBlocks.flatMap((sb) => {
17741835
const ids = [sb.id]
17751836
if (sb.canonicalParamId) ids.push(sb.canonicalParamId)
17761837
const cId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id]
@@ -1785,57 +1846,45 @@ export const ToolInput = memo(function ToolInput({
17851846
})
17861847
)
17871848

1788-
displaySubBlocks.forEach((sb) => {
1789-
const effectiveParamId = sb.id
1790-
const canonicalId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id]
1791-
const canonicalGroup = canonicalId
1792-
? toolCanonicalIndex?.groupsById[canonicalId]
1793-
: undefined
1794-
const hasCanonicalPair = isCanonicalPair(canonicalGroup)
1795-
const canonicalMode =
1796-
canonicalGroup && hasCanonicalPair
1797-
? resolveCanonicalMode(
1798-
canonicalGroup,
1799-
{ operation: tool.operation, ...tool.params },
1800-
toolScopedOverrides
1801-
)
1802-
: undefined
1803-
1804-
const canonicalToggleProp =
1805-
hasCanonicalPair && canonicalMode && canonicalId
1806-
? {
1807-
mode: canonicalMode,
1808-
onToggle: () => {
1809-
const nextMode =
1810-
canonicalMode === 'advanced' ? 'basic' : 'advanced'
1811-
collaborativeSetBlockCanonicalMode(
1812-
blockId,
1813-
`${tool.type}:${canonicalId}`,
1814-
nextMode
1815-
)
1816-
},
1817-
}
1818-
: undefined
1849+
type RenderItem =
1850+
| { kind: 'subblock'; sb: BlockSubBlockConfig }
1851+
| { kind: 'oauth' }
18191852

1820-
const sbWithTitle = sb.title
1821-
? sb
1822-
: { ...sb, title: formatParameterLabel(effectiveParamId) }
1853+
const renderOrder: RenderItem[] = displaySubBlocks.map((sb) => ({
1854+
kind: 'subblock' as const,
1855+
sb,
1856+
}))
18231857

1824-
renderedElements.push(
1825-
<ToolSubBlockRenderer
1826-
key={sb.id}
1827-
blockId={blockId}
1828-
subBlockId={subBlockId}
1829-
toolIndex={toolIndex}
1830-
subBlock={sbWithTitle}
1831-
effectiveParamId={effectiveParamId}
1832-
toolParams={tool.params}
1833-
onParamChange={handleParamChange}
1834-
disabled={disabled}
1835-
canonicalToggle={canonicalToggleProp}
1836-
/>
1858+
if (showOAuth) {
1859+
const credentialIdx = allBlockSubBlocks.findIndex(
1860+
(sb) => sb.type === 'oauth-input'
18371861
)
1838-
})
1862+
if (credentialIdx >= 0) {
1863+
const sbPositions = new Map(allBlockSubBlocks.map((sb, i) => [sb.id, i]))
1864+
const insertAt = renderOrder.findIndex(
1865+
(item) =>
1866+
item.kind === 'subblock' &&
1867+
(sbPositions.get(item.sb.id) ?? Number.POSITIVE_INFINITY) >
1868+
credentialIdx
1869+
)
1870+
if (insertAt === -1) {
1871+
renderOrder.push({ kind: 'oauth' })
1872+
} else {
1873+
renderOrder.splice(insertAt, 0, { kind: 'oauth' })
1874+
}
1875+
} else {
1876+
renderOrder.unshift({ kind: 'oauth' })
1877+
}
1878+
}
1879+
1880+
for (const item of renderOrder) {
1881+
if (item.kind === 'oauth') {
1882+
const el = renderOAuthAccount()
1883+
if (el) renderedElements.push(el)
1884+
} else {
1885+
renderedElements.push(renderSubBlock(item.sb))
1886+
}
1887+
}
18391888

18401889
const uncoveredParams = displayParams.filter(
18411890
(param) =>
@@ -1873,6 +1922,11 @@ export const ToolInput = memo(function ToolInput({
18731922
)
18741923
}
18751924

1925+
{
1926+
const el = renderOAuthAccount()
1927+
if (el) renderedElements.push(el)
1928+
}
1929+
18761930
const filteredParams = displayParams.filter((param) =>
18771931
evaluateParameterCondition(param, tool)
18781932
)

apps/sim/blocks/blocks/schedule.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,25 @@ export const ScheduleBlock: BlockConfig = {
122122
required: true,
123123
mode: 'trigger',
124124
condition: { field: 'scheduleType', value: 'custom' },
125+
wandConfig: {
126+
enabled: true,
127+
prompt: `You are an expert at writing cron expressions. Generate a valid cron expression based on the user's description.
128+
129+
Cron format: minute hour day-of-month month day-of-week
130+
- minute: 0-59
131+
- hour: 0-23
132+
- day-of-month: 1-31
133+
- month: 1-12
134+
- day-of-week: 0-7 (0 and 7 are Sunday)
135+
136+
Special characters: * (any), , (list), - (range), / (step)
137+
138+
{context}
139+
140+
Return ONLY the cron expression, nothing else. No explanation, no backticks, no quotes.`,
141+
placeholder: 'Describe your schedule (e.g., "every weekday at 9am")',
142+
generationType: 'cron-expression',
143+
},
125144
},
126145

127146
{

apps/sim/blocks/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export type GenerationType =
4040
| 'neo4j-parameters'
4141
| 'timestamp'
4242
| 'timezone'
43+
| 'cron-expression'
4344

4445
export type SubBlockType =
4546
| 'short-input' // Single line input

apps/sim/tools/params.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -827,11 +827,10 @@ export function formatParameterLabel(paramId: string): string {
827827
}
828828

829829
/**
830-
* SubBlock IDs that are "structural" — they control tool routing or auth,
831-
* not user-facing parameters. These are excluded from tool-input rendering
832-
* unless they have an explicit paramVisibility set.
830+
* SubBlock IDs that control tool routing, not user-facing parameters.
831+
* Excluded from tool-input rendering unless they have an explicit paramVisibility set.
833832
*/
834-
const STRUCTURAL_SUBBLOCK_IDS = new Set(['operation', 'authMethod', 'destinationType'])
833+
const STRUCTURAL_SUBBLOCK_IDS = new Set(['operation'])
835834

836835
/**
837836
* SubBlock types that represent auth/credential inputs handled separately
@@ -955,12 +954,8 @@ export function getSubBlocksForToolInput(
955954
} else if (sb.id in toolParamVisibility) {
956955
visibility = toolParamVisibility[sb.id]
957956
} else if (sb.canonicalParamId) {
958-
// SubBlock has a canonicalParamId that doesn't directly match a tool param.
959-
// This means the block's params() function transforms it before sending to the tool
960-
// (e.g. listFolderId → folderId). These are user-facing inputs, default to user-or-llm.
961957
visibility = 'user-or-llm'
962958
} else {
963-
// SubBlock has no corresponding tool param — skip it
964959
continue
965960
}
966961
}

apps/sim/tools/slack/message.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const slackMessageTool: ToolConfig<SlackMessageParams, SlackMessageRespon
5757
visibility: 'user-or-llm',
5858
description: 'Message text to send (supports Slack mrkdwn formatting)',
5959
},
60-
thread_ts: {
60+
threadTs: {
6161
type: 'string',
6262
required: false,
6363
visibility: 'user-or-llm',
@@ -84,7 +84,7 @@ export const slackMessageTool: ToolConfig<SlackMessageParams, SlackMessageRespon
8484
channel: isDM ? undefined : params.channel,
8585
userId: isDM ? params.dmUserId : params.userId,
8686
text: params.text,
87-
thread_ts: params.thread_ts || undefined,
87+
thread_ts: params.threadTs || undefined,
8888
files: params.files || null,
8989
}
9090
},

apps/sim/tools/slack/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ export interface SlackMessageParams extends SlackBaseParams {
516516
dmUserId?: string
517517
userId?: string
518518
text: string
519-
thread_ts?: string
519+
threadTs?: string
520520
files?: UserFile[]
521521
}
522522

0 commit comments

Comments
 (0)