Compare commits

...

3 Commits

Author SHA1 Message Date
waleed
9697710569 fix(trigger): add isolated-vm support to trigger.dev container builds
Scheduled workflow executions running in trigger.dev containers were
failing to spawn isolated-vm workers because the native module wasn't
available in the container. This caused loop condition evaluation to
silently fail and exit after one iteration.

- Add isolated-vm to build.external and additionalPackages in trigger config
- Include isolated-vm-worker.cjs via additionalFiles for child process spawning
- Add fallback path resolution for worker file in trigger.dev environment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:33:02 -08:00
Vikhyath Mondreti
68f44b8df4 improvement(resolver): resovled empty sentinel to not pass through unexecuted valid refs to text inputs (#3266) 2026-02-20 01:56:33 -08:00
Waleed
9920882dc5 fix(blocks): move type coercions from tools.config.tool to tools.config.params (#3264)
* fix(blocks): move type coercions from tools.config.tool to tools.config.params

Number() coercions in tools.config.tool ran at serialization time before
variable resolution, destroying dynamic references like <block.result.count>
by converting them to NaN/null. Moved all coercions to tools.config.params
which runs at execution time after variables are resolved.

Fixed in 15 blocks: exa, arxiv, sentry, incidentio, wikipedia, ahrefs,
posthog, elasticsearch, dropbox, hunter, lemlist, spotify, youtube, grafana,
parallel. Also added mode: 'advanced' to optional exa fields.

Closes #3258

* fix(blocks): address PR review — move remaining param mutations from tool() to params()

- Moved field mappings from tool() to params() in grafana, posthog,
  lemlist, spotify, dropbox (same dynamic reference bug)
- Fixed parallel.ts excerpts/full_content boolean logic
- Fixed parallel.ts search_queries empty case (must set undefined)
- Fixed elasticsearch.ts timeout not included when already ends with 's'
- Restored dropbox.ts tool() switch for proper default fallback

* fix(blocks): restore field renames to tool() for serialization-time validation

Field renames (e.g. personalApiKey→apiKey) must be in tool() because
validateRequiredFieldsBeforeExecution calls selectToolId()→tool() then
checks renamed field names on params. Only type coercions (Number(),
boolean) stay in params() to avoid destroying dynamic variable references.
2026-02-19 21:54:16 -08:00
23 changed files with 260 additions and 268 deletions

View File

@@ -454,6 +454,8 @@ Enables AI-assisted field generation.
## Tools Configuration
**Important:** `tools.config.tool` runs during serialization before variable resolution. Put `Number()` and other type coercions in `tools.config.params` instead, which runs at execution time after variables are resolved.
**Preferred:** Use tool names directly as dropdown option IDs to avoid switch cases:
```typescript
// Dropdown options use tool IDs directly

View File

@@ -238,7 +238,7 @@ export const ServiceBlock: BlockConfig = {
bgColor: '#hexcolor',
icon: ServiceIcon,
subBlocks: [ /* see SubBlock Properties */ ],
tools: { access: ['service_action'], config: { tool: (p) => `service_${p.operation}` } },
tools: { access: ['service_action'], config: { tool: (p) => `service_${p.operation}`, params: (p) => ({ /* type coercions here */ }) } },
inputs: { /* ... */ },
outputs: { /* ... */ },
}
@@ -246,6 +246,8 @@ export const ServiceBlock: BlockConfig = {
Register in `blocks/registry.ts` (alphabetically).
**Important:** `tools.config.tool` runs during serialization (before variable resolution). Never do `Number()` or other type coercions there — dynamic references like `<Block.output>` will be destroyed. Use `tools.config.params` for type coercions (it runs during execution, after variables are resolved).
**SubBlock Properties:**
```typescript
{

View File

@@ -485,14 +485,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
],
config: {
tool: (params) => {
// Convert numeric string inputs to numbers
if (params.limit) {
params.limit = Number(params.limit)
}
if (params.offset) {
params.offset = Number(params.offset)
}
switch (params.operation) {
case 'ahrefs_domain_rating':
return 'ahrefs_domain_rating'
@@ -514,6 +506,12 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
return 'ahrefs_domain_rating'
}
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.limit) result.limit = Number(params.limit)
if (params.offset) result.offset = Number(params.offset)
return result
},
},
},
inputs: {

View File

@@ -110,11 +110,6 @@ export const ArxivBlock: BlockConfig<ArxivResponse> = {
access: ['arxiv_search', 'arxiv_get_paper', 'arxiv_get_author_papers'],
config: {
tool: (params) => {
// Convert maxResults to a number for operations that use it
if (params.maxResults) {
params.maxResults = Number(params.maxResults)
}
switch (params.operation) {
case 'arxiv_search':
return 'arxiv_search'
@@ -126,6 +121,11 @@ export const ArxivBlock: BlockConfig<ArxivResponse> = {
return 'arxiv_search'
}
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.maxResults) result.maxResults = Number(params.maxResults)
return result
},
},
},
inputs: {

View File

@@ -309,20 +309,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
],
config: {
tool: (params) => {
// Convert numeric params
if (params.limit) {
params.limit = Number(params.limit)
}
if (params.maxResults) {
params.maxResults = Number(params.maxResults)
}
// Normalize file input for upload operation - use canonical 'file' param
const normalizedFile = normalizeFileInput(params.file, { single: true })
if (normalizedFile) {
params.file = normalizedFile
}
switch (params.operation) {
case 'dropbox_upload':
return 'dropbox_upload'
@@ -348,6 +334,16 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
return 'dropbox_upload'
}
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.limit) result.limit = Number(params.limit)
if (params.maxResults) result.maxResults = Number(params.maxResults)
const normalizedFile = normalizeFileInput(params.file, { single: true })
if (normalizedFile) {
result.file = normalizedFile
}
return result
},
},
},
inputs: {

View File

@@ -457,24 +457,19 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
],
config: {
tool: (params) => {
// Convert numeric strings to numbers
if (params.size) {
params.size = Number(params.size)
}
if (params.from) {
params.from = Number(params.from)
}
if (params.retryOnConflict) {
params.retryOnConflict = Number(params.retryOnConflict)
}
// Append 's' to timeout for Elasticsearch time format
if (params.timeout && !params.timeout.endsWith('s')) {
params.timeout = `${params.timeout}s`
}
// Return the operation as the tool ID
return params.operation || 'elasticsearch_search'
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.size) result.size = Number(params.size)
if (params.from) result.from = Number(params.from)
if (params.retryOnConflict) result.retryOnConflict = Number(params.retryOnConflict)
if (params.timeout && typeof params.timeout === 'string') {
result.timeout = params.timeout.endsWith('s') ? params.timeout : `${params.timeout}s`
}
return result
},
},
},

View File

@@ -49,6 +49,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
title: 'Use Autoprompt',
type: 'switch',
condition: { field: 'operation', value: 'exa_search' },
mode: 'advanced',
},
{
id: 'type',
@@ -62,6 +63,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
],
value: () => 'auto',
condition: { field: 'operation', value: 'exa_search' },
mode: 'advanced',
},
{
id: 'includeDomains',
@@ -69,6 +71,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
type: 'long-input',
placeholder: 'example.com, another.com (comma-separated)',
condition: { field: 'operation', value: 'exa_search' },
mode: 'advanced',
},
{
id: 'excludeDomains',
@@ -76,6 +79,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
type: 'long-input',
placeholder: 'exclude.com, another.com (comma-separated)',
condition: { field: 'operation', value: 'exa_search' },
mode: 'advanced',
},
{
id: 'category',
@@ -95,6 +99,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
],
value: () => '',
condition: { field: 'operation', value: 'exa_search' },
mode: 'advanced',
},
{
id: 'text',
@@ -107,12 +112,14 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
title: 'Include Highlights',
type: 'switch',
condition: { field: 'operation', value: 'exa_search' },
mode: 'advanced',
},
{
id: 'summary',
title: 'Include Summary',
type: 'switch',
condition: { field: 'operation', value: 'exa_search' },
mode: 'advanced',
},
{
id: 'livecrawl',
@@ -125,6 +132,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
],
value: () => 'never',
condition: { field: 'operation', value: 'exa_search' },
mode: 'advanced',
},
// Get Contents operation inputs
{
@@ -147,6 +155,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
type: 'long-input',
placeholder: 'Enter a query to guide the summary generation...',
condition: { field: 'operation', value: 'exa_get_contents' },
mode: 'advanced',
},
{
id: 'subpages',
@@ -154,6 +163,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
type: 'short-input',
placeholder: '5',
condition: { field: 'operation', value: 'exa_get_contents' },
mode: 'advanced',
},
{
id: 'subpageTarget',
@@ -161,12 +171,14 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
type: 'long-input',
placeholder: 'docs, tutorial, about (comma-separated)',
condition: { field: 'operation', value: 'exa_get_contents' },
mode: 'advanced',
},
{
id: 'highlights',
title: 'Include Highlights',
type: 'switch',
condition: { field: 'operation', value: 'exa_get_contents' },
mode: 'advanced',
},
// Find Similar Links operation inputs
{
@@ -196,6 +208,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
type: 'long-input',
placeholder: 'example.com, another.com (comma-separated)',
condition: { field: 'operation', value: 'exa_find_similar_links' },
mode: 'advanced',
},
{
id: 'excludeDomains',
@@ -203,12 +216,14 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
type: 'long-input',
placeholder: 'exclude.com, another.com (comma-separated)',
condition: { field: 'operation', value: 'exa_find_similar_links' },
mode: 'advanced',
},
{
id: 'excludeSourceDomain',
title: 'Exclude Source Domain',
type: 'switch',
condition: { field: 'operation', value: 'exa_find_similar_links' },
mode: 'advanced',
},
{
id: 'category',
@@ -228,18 +243,21 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
],
value: () => '',
condition: { field: 'operation', value: 'exa_find_similar_links' },
mode: 'advanced',
},
{
id: 'highlights',
title: 'Include Highlights',
type: 'switch',
condition: { field: 'operation', value: 'exa_find_similar_links' },
mode: 'advanced',
},
{
id: 'summary',
title: 'Include Summary',
type: 'switch',
condition: { field: 'operation', value: 'exa_find_similar_links' },
mode: 'advanced',
},
{
id: 'livecrawl',
@@ -252,6 +270,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
],
value: () => 'never',
condition: { field: 'operation', value: 'exa_find_similar_links' },
mode: 'advanced',
},
// Answer operation inputs
{
@@ -267,6 +286,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
title: 'Include Text',
type: 'switch',
condition: { field: 'operation', value: 'exa_answer' },
mode: 'advanced',
},
// Research operation inputs
{
@@ -309,16 +329,6 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
],
config: {
tool: (params) => {
// Convert numResults to a number for operations that use it
if (params.numResults) {
params.numResults = Number(params.numResults)
}
// Convert subpages to a number if provided
if (params.subpages) {
params.subpages = Number(params.subpages)
}
switch (params.operation) {
case 'exa_search':
return 'exa_search'
@@ -334,6 +344,16 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
return 'exa_search'
}
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.numResults) {
result.numResults = Number(params.numResults)
}
if (params.subpages) {
result.subpages = Number(params.subpages)
}
return result
},
},
},
inputs: {

View File

@@ -606,45 +606,23 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
],
config: {
tool: (params) => {
// Convert numeric string fields to numbers
if (params.panelId) {
params.panelId = Number(params.panelId)
}
if (params.annotationId) {
params.annotationId = Number(params.annotationId)
}
if (params.time) {
params.time = Number(params.time)
}
if (params.timeEnd) {
params.timeEnd = Number(params.timeEnd)
}
if (params.from) {
params.from = Number(params.from)
}
if (params.to) {
params.to = Number(params.to)
}
// Map subblock fields to tool parameter names
if (params.alertTitle) {
params.title = params.alertTitle
}
if (params.folderTitle) {
params.title = params.folderTitle
}
if (params.folderUidNew) {
params.uid = params.folderUidNew
}
if (params.annotationTags) {
params.tags = params.annotationTags
}
if (params.annotationDashboardUid) {
params.dashboardUid = params.annotationDashboardUid
}
if (params.alertTitle) params.title = params.alertTitle
if (params.folderTitle) params.title = params.folderTitle
if (params.folderUidNew) params.uid = params.folderUidNew
if (params.annotationTags) params.tags = params.annotationTags
if (params.annotationDashboardUid) params.dashboardUid = params.annotationDashboardUid
return params.operation
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.panelId) result.panelId = Number(params.panelId)
if (params.annotationId) result.annotationId = Number(params.annotationId)
if (params.time) result.time = Number(params.time)
if (params.timeEnd) result.timeEnd = Number(params.timeEnd)
if (params.from) result.from = Number(params.from)
if (params.to) result.to = Number(params.to)
return result
},
},
},
inputs: {

View File

@@ -204,11 +204,6 @@ Return ONLY the search query text - no explanations.`,
],
config: {
tool: (params) => {
// Convert numeric parameters
if (params.limit) {
params.limit = Number(params.limit)
}
switch (params.operation) {
case 'hunter_discover':
return 'hunter_discover'
@@ -226,6 +221,11 @@ Return ONLY the search query text - no explanations.`,
return 'hunter_domain_search'
}
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.limit) result.limit = Number(params.limit)
return result
},
},
},
inputs: {

View File

@@ -826,16 +826,6 @@ Return ONLY the JSON array - no explanations or markdown formatting.`,
],
config: {
tool: (params) => {
// Convert page_size to a number if provided
if (params.page_size) {
params.page_size = Number(params.page_size)
}
// Convert notify_incident_channel from string to boolean
if (params.notify_incident_channel !== undefined) {
params.notify_incident_channel = params.notify_incident_channel === 'true'
}
switch (params.operation) {
case 'incidentio_incidents_list':
return 'incidentio_incidents_list'
@@ -929,6 +919,14 @@ Return ONLY the JSON array - no explanations or markdown formatting.`,
return 'incidentio_incidents_list'
}
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.page_size) result.page_size = Number(params.page_size)
if (params.notify_incident_channel !== undefined) {
result.notify_incident_channel = params.notify_incident_channel === 'true'
}
return result
},
},
},
inputs: {

View File

@@ -169,17 +169,7 @@ export const LemlistBlock: BlockConfig<LemlistResponse> = {
access: ['lemlist_get_activities', 'lemlist_get_lead', 'lemlist_send_email'],
config: {
tool: (params) => {
if (params.limit) {
params.limit = Number(params.limit)
}
if (params.offset) {
params.offset = Number(params.offset)
}
// Map filterLeadId to leadId for get_activities tool
if (params.filterLeadId) {
params.leadId = params.filterLeadId
}
if (params.filterLeadId) params.leadId = params.filterLeadId
switch (params.operation) {
case 'get_activities':
return 'lemlist_get_activities'
@@ -191,6 +181,12 @@ export const LemlistBlock: BlockConfig<LemlistResponse> = {
return 'lemlist_get_activities'
}
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.limit) result.limit = Number(params.limit)
if (params.offset) result.offset = Number(params.offset)
return result
},
},
},
inputs: {

View File

@@ -149,66 +149,48 @@ export const ParallelBlock: BlockConfig<ToolResponse> = {
access: ['parallel_search', 'parallel_extract', 'parallel_deep_research'],
config: {
tool: (params) => {
if (params.extract_objective) params.objective = params.extract_objective
if (params.research_input) params.input = params.research_input
switch (params.operation) {
case 'search':
// Convert search_queries from comma-separated string to array (if provided)
if (params.search_queries && typeof params.search_queries === 'string') {
const queries = params.search_queries
.split(',')
.map((query: string) => query.trim())
.filter((query: string) => query.length > 0)
// Only set if we have actual queries
if (queries.length > 0) {
params.search_queries = queries
} else {
params.search_queries = undefined
}
}
// Convert numeric parameters
if (params.max_results) {
params.max_results = Number(params.max_results)
}
if (params.max_chars_per_result) {
params.max_chars_per_result = Number(params.max_chars_per_result)
}
return 'parallel_search'
case 'extract':
// Map extract_objective to objective for the tool
params.objective = params.extract_objective
// Convert boolean strings to actual booleans with defaults
if (params.excerpts === 'true' || params.excerpts === true) {
params.excerpts = true
} else if (params.excerpts === 'false' || params.excerpts === false) {
params.excerpts = false
} else {
// Default to true if not provided
params.excerpts = true
}
if (params.full_content === 'true' || params.full_content === true) {
params.full_content = true
} else if (params.full_content === 'false' || params.full_content === false) {
params.full_content = false
} else {
// Default to false if not provided
params.full_content = false
}
return 'parallel_extract'
case 'deep_research':
// Map research_input to input for the tool
params.input = params.research_input
return 'parallel_deep_research'
default:
return 'parallel_search'
}
},
params: (params) => {
const result: Record<string, unknown> = {}
const operation = params.operation
if (operation === 'search') {
if (params.search_queries && typeof params.search_queries === 'string') {
const queries = params.search_queries
.split(',')
.map((query: string) => query.trim())
.filter((query: string) => query.length > 0)
if (queries.length > 0) {
result.search_queries = queries
} else {
result.search_queries = undefined
}
}
if (params.max_results) result.max_results = Number(params.max_results)
if (params.max_chars_per_result) {
result.max_chars_per_result = Number(params.max_chars_per_result)
}
}
if (operation === 'extract') {
result.excerpts = !(params.excerpts === 'false' || params.excerpts === false)
result.full_content = params.full_content === 'true' || params.full_content === true
}
return result
},
},
},
inputs: {

View File

@@ -1185,22 +1185,15 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
],
config: {
tool: (params) => {
// Convert numeric parameters
if (params.limit) params.limit = Number(params.limit)
if (params.offset) params.offset = Number(params.offset)
if (params.rolloutPercentage) params.rolloutPercentage = Number(params.rolloutPercentage)
// Map projectIdParam to projectId for get_project operation
// Field renames in tool() are safe (they copy values, not coerce types)
// and are needed for serialization-time validation of required fields
if (params.operation === 'posthog_get_project' && params.projectIdParam) {
params.projectId = params.projectIdParam
}
// Map personalApiKey to apiKey for all private endpoint tools
if (params.personalApiKey) {
params.apiKey = params.personalApiKey
}
// Map featureFlagId to flagId for feature flag operations
const flagOps = [
'posthog_get_feature_flag',
'posthog_update_feature_flag',
@@ -1210,7 +1203,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
params.flagId = params.featureFlagId
}
// Map surveyType to type for survey operations
if (
(params.operation === 'posthog_create_survey' ||
params.operation === 'posthog_update_survey') &&
@@ -1219,37 +1211,30 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
params.type = params.surveyType
}
// Map isStatic for cohorts
if (params.operation === 'posthog_create_cohort' && params.isStatic !== undefined) {
params.is_static = params.isStatic
}
// Map dateMarker to date_marker for annotations
if (params.operation === 'posthog_create_annotation' && params.dateMarker) {
params.date_marker = params.dateMarker
}
// Map propertyType to property_type
if (params.operation === 'posthog_update_property_definition' && params.propertyType) {
params.property_type = params.propertyType
}
// Map insightQuery to query for insights
if (params.operation === 'posthog_create_insight' && params.insightQuery) {
params.query = params.insightQuery
}
// Map insightTags to tags for insights
if (params.operation === 'posthog_create_insight' && params.insightTags) {
params.tags = params.insightTags
}
// Map distinctIdFilter to distinctId for list_persons
if (params.operation === 'posthog_list_persons' && params.distinctIdFilter) {
params.distinctId = params.distinctIdFilter
}
// Map experiment date fields
if (params.operation === 'posthog_create_experiment') {
if (params.experimentStartDate) {
params.startDate = params.experimentStartDate
@@ -1259,7 +1244,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
}
}
// Map survey date fields
if (
params.operation === 'posthog_create_survey' ||
params.operation === 'posthog_update_survey'
@@ -1272,13 +1256,17 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
}
}
// Convert responsesLimit to number
if (params.responsesLimit) {
params.responsesLimit = Number(params.responsesLimit)
}
return params.operation as string
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.limit) result.limit = Number(params.limit)
if (params.offset) result.offset = Number(params.offset)
if (params.rolloutPercentage) result.rolloutPercentage = Number(params.rolloutPercentage)
if (params.responsesLimit) result.responsesLimit = Number(params.responsesLimit)
return result
},
},
},

View File

@@ -602,11 +602,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
],
config: {
tool: (params) => {
// Convert numeric fields
if (params.limit) {
params.limit = Number(params.limit)
}
// Return the appropriate tool based on operation
switch (params.operation) {
case 'sentry_issues_list':
@@ -637,6 +632,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
return 'sentry_issues_list'
}
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.limit) result.limit = Number(params.limit)
return result
},
},
},
inputs: {

View File

@@ -755,43 +755,24 @@ export const SpotifyBlock: BlockConfig<ToolResponse> = {
],
config: {
tool: (params) => {
// Convert numeric parameters
if (params.limit) {
params.limit = Number(params.limit)
}
if (params.volume_percent) {
params.volume_percent = Number(params.volume_percent)
}
if (params.range_start) {
params.range_start = Number(params.range_start)
}
if (params.insert_before) {
params.insert_before = Number(params.insert_before)
}
if (params.range_length) {
params.range_length = Number(params.range_length)
}
if (params.position_ms) {
params.position_ms = Number(params.position_ms)
}
// Map followType to type for check_following
if (params.followType) {
params.type = params.followType
}
// Map newName to name for update_playlist
if (params.newName) {
params.name = params.newName
}
// Map playUris to uris for play
if (params.playUris) {
params.uris = params.playUris
}
// Normalize file input for cover image
if (params.coverImage !== undefined) {
params.coverImage = normalizeFileInput(params.coverImage, { single: true })
}
if (params.followType) params.type = params.followType
if (params.newName) params.name = params.newName
if (params.playUris) params.uris = params.playUris
return params.operation || 'spotify_search'
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.limit) result.limit = Number(params.limit)
if (params.volume_percent) result.volume_percent = Number(params.volume_percent)
if (params.range_start) result.range_start = Number(params.range_start)
if (params.insert_before) result.insert_before = Number(params.insert_before)
if (params.range_length) result.range_length = Number(params.range_length)
if (params.position_ms) result.position_ms = Number(params.position_ms)
if (params.coverImage !== undefined) {
result.coverImage = normalizeFileInput(params.coverImage, { single: true })
}
return result
},
},
},
inputs: {

View File

@@ -64,11 +64,6 @@ export const WikipediaBlock: BlockConfig<WikipediaResponse> = {
access: ['wikipedia_summary', 'wikipedia_search', 'wikipedia_content', 'wikipedia_random'],
config: {
tool: (params) => {
// Convert searchLimit to a number for search operation
if (params.searchLimit) {
params.searchLimit = Number(params.searchLimit)
}
switch (params.operation) {
case 'wikipedia_summary':
return 'wikipedia_summary'
@@ -82,6 +77,11 @@ export const WikipediaBlock: BlockConfig<WikipediaResponse> = {
return 'wikipedia_summary'
}
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.searchLimit) result.searchLimit = Number(params.searchLimit)
return result
},
},
},
inputs: {

View File

@@ -442,11 +442,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
],
config: {
tool: (params) => {
// Convert numeric parameters
if (params.maxResults) {
params.maxResults = Number(params.maxResults)
}
switch (params.operation) {
case 'youtube_search':
return 'youtube_search'
@@ -470,6 +465,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
return 'youtube_search'
}
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.maxResults) result.maxResults = Number(params.maxResults)
return result
},
},
},
inputs: {

View File

@@ -7,7 +7,11 @@ import { BlockResolver } from '@/executor/variables/resolvers/block'
import { EnvResolver } from '@/executor/variables/resolvers/env'
import { LoopResolver } from '@/executor/variables/resolvers/loop'
import { ParallelResolver } from '@/executor/variables/resolvers/parallel'
import type { ResolutionContext, Resolver } from '@/executor/variables/resolvers/reference'
import {
RESOLVED_EMPTY,
type ResolutionContext,
type Resolver,
} from '@/executor/variables/resolvers/reference'
import { WorkflowResolver } from '@/executor/variables/resolvers/workflow'
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
@@ -104,7 +108,11 @@ export class VariableResolver {
loopScope,
}
return this.resolveReference(trimmed, resolutionContext)
const result = this.resolveReference(trimmed, resolutionContext)
if (result === RESOLVED_EMPTY) {
return null
}
return result
}
}
@@ -174,6 +182,13 @@ export class VariableResolver {
return match
}
if (resolved === RESOLVED_EMPTY) {
if (blockType === BlockType.FUNCTION) {
return this.blockResolver.formatValueForBlock(null, blockType, language)
}
return ''
}
return this.blockResolver.formatValueForBlock(resolved, blockType, language)
} catch (error) {
replacementError = error instanceof Error ? error : new Error(String(error))
@@ -207,7 +222,6 @@ export class VariableResolver {
let replacementError: Error | null = null
// Use generic utility for smart variable reference replacement
let result = replaceValidReferences(template, (match) => {
if (replacementError) return match
@@ -217,6 +231,10 @@ export class VariableResolver {
return match
}
if (resolved === RESOLVED_EMPTY) {
return 'null'
}
if (typeof resolved === 'string') {
const escaped = resolved.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
return `'${escaped}'`

View File

@@ -2,7 +2,7 @@ import { loggerMock } from '@sim/testing'
import { describe, expect, it, vi } from 'vitest'
import { ExecutionState } from '@/executor/execution/state'
import { BlockResolver } from './block'
import type { ResolutionContext } from './reference'
import { RESOLVED_EMPTY, type ResolutionContext } from './reference'
vi.mock('@sim/logger', () => loggerMock)
vi.mock('@/blocks/registry', async () => {
@@ -134,15 +134,18 @@ describe('BlockResolver', () => {
expect(resolver.resolve('<source.items.1.id>', ctx)).toBe(2)
})
it.concurrent('should return undefined for non-existent path when no schema defined', () => {
const workflow = createTestWorkflow([{ id: 'source', type: 'unknown_block_type' }])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
source: { existing: 'value' },
})
it.concurrent(
'should return RESOLVED_EMPTY for non-existent path when no schema defined',
() => {
const workflow = createTestWorkflow([{ id: 'source', type: 'unknown_block_type' }])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
source: { existing: 'value' },
})
expect(resolver.resolve('<source.nonexistent>', ctx)).toBeUndefined()
})
expect(resolver.resolve('<source.nonexistent>', ctx)).toBe(RESOLVED_EMPTY)
}
)
it.concurrent('should throw error for path not in output schema', () => {
const workflow = createTestWorkflow([
@@ -162,7 +165,7 @@ describe('BlockResolver', () => {
expect(() => resolver.resolve('<source.invalidField>', ctx)).toThrow(/Available fields:/)
})
it.concurrent('should return undefined for path in schema but missing in data', () => {
it.concurrent('should return RESOLVED_EMPTY for path in schema but missing in data', () => {
const workflow = createTestWorkflow([
{
id: 'source',
@@ -175,7 +178,7 @@ describe('BlockResolver', () => {
})
expect(resolver.resolve('<source.stdout>', ctx)).toBe('log output')
expect(resolver.resolve('<source.result>', ctx)).toBeUndefined()
expect(resolver.resolve('<source.result>', ctx)).toBe(RESOLVED_EMPTY)
})
it.concurrent(
@@ -191,7 +194,7 @@ describe('BlockResolver', () => {
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {})
expect(resolver.resolve('<workflow.childTraceSpans>', ctx)).toBeUndefined()
expect(resolver.resolve('<workflow.childTraceSpans>', ctx)).toBe(RESOLVED_EMPTY)
}
)
@@ -208,7 +211,7 @@ describe('BlockResolver', () => {
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {})
expect(resolver.resolve('<workflowinput.childTraceSpans>', ctx)).toBeUndefined()
expect(resolver.resolve('<workflowinput.childTraceSpans>', ctx)).toBe(RESOLVED_EMPTY)
}
)
@@ -225,13 +228,13 @@ describe('BlockResolver', () => {
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {})
expect(resolver.resolve('<hitl.response>', ctx)).toBeUndefined()
expect(resolver.resolve('<hitl.submission>', ctx)).toBeUndefined()
expect(resolver.resolve('<hitl.resumeInput>', ctx)).toBeUndefined()
expect(resolver.resolve('<hitl.response>', ctx)).toBe(RESOLVED_EMPTY)
expect(resolver.resolve('<hitl.submission>', ctx)).toBe(RESOLVED_EMPTY)
expect(resolver.resolve('<hitl.resumeInput>', ctx)).toBe(RESOLVED_EMPTY)
}
)
it.concurrent('should return undefined for non-existent block', () => {
it.concurrent('should return undefined for block not in workflow', () => {
const workflow = createTestWorkflow([{ id: 'existing' }])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {})
@@ -239,6 +242,21 @@ describe('BlockResolver', () => {
expect(resolver.resolve('<nonexistent>', ctx)).toBeUndefined()
})
it.concurrent('should return RESOLVED_EMPTY for block in workflow that did not execute', () => {
const workflow = createTestWorkflow([
{ id: 'start-block', name: 'Start', type: 'start_trigger' },
{ id: 'slack-block', name: 'Slack', type: 'slack_trigger' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'slack-block': { message: 'hello from slack' },
})
expect(resolver.resolve('<slack.message>', ctx)).toBe('hello from slack')
expect(resolver.resolve('<start>', ctx)).toBe(RESOLVED_EMPTY)
expect(resolver.resolve('<start.input>', ctx)).toBe(RESOLVED_EMPTY)
})
it.concurrent('should fall back to context blockStates', () => {
const workflow = createTestWorkflow([{ id: 'source' }])
const resolver = new BlockResolver(workflow)
@@ -1012,24 +1030,24 @@ describe('BlockResolver', () => {
expect(resolver.resolve('<source.other>', ctx)).toBe('exists')
})
it.concurrent('should handle output with undefined values', () => {
it.concurrent('should return RESOLVED_EMPTY for output with undefined values', () => {
const workflow = createTestWorkflow([{ id: 'source', type: 'unknown_block_type' }])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
source: { value: undefined, other: 'exists' },
})
expect(resolver.resolve('<source.value>', ctx)).toBeUndefined()
expect(resolver.resolve('<source.value>', ctx)).toBe(RESOLVED_EMPTY)
})
it.concurrent('should return undefined for deeply nested non-existent path', () => {
it.concurrent('should return RESOLVED_EMPTY for deeply nested non-existent path', () => {
const workflow = createTestWorkflow([{ id: 'source', type: 'unknown_block_type' }])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
source: { level1: { level2: {} } },
})
expect(resolver.resolve('<source.level1.level2.level3>', ctx)).toBeUndefined()
expect(resolver.resolve('<source.level1.level2.level3>', ctx)).toBe(RESOLVED_EMPTY)
})
})
})

View File

@@ -13,6 +13,7 @@ import {
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
import {
navigatePath,
RESOLVED_EMPTY,
type ResolutionContext,
type Resolver,
} from '@/executor/variables/resolvers/reference'
@@ -84,7 +85,12 @@ export class BlockResolver implements Resolver {
return result.value
}
return this.handleBackwardsCompat(block, output, pathParts)
const backwardsCompat = this.handleBackwardsCompat(block, output, pathParts)
if (backwardsCompat !== undefined) {
return backwardsCompat
}
return RESOLVED_EMPTY
} catch (error) {
if (error instanceof InvalidFieldError) {
const fallback = this.handleBackwardsCompat(block, output, pathParts)

View File

@@ -12,6 +12,14 @@ export interface Resolver {
resolve(reference: string, context: ResolutionContext): any
}
/**
* Sentinel value indicating a reference was resolved to a known block
* that produced no output (e.g., the block exists in the workflow but
* didn't execute on this path). Distinct from `undefined`, which means
* the reference couldn't be matched to any block at all.
*/
export const RESOLVED_EMPTY = Symbol('RESOLVED_EMPTY')
/**
* Navigate through nested object properties using a path array.
* Supports dot notation and array indices.

View File

@@ -679,11 +679,15 @@ function spawnWorker(): Promise<WorkerInfo> {
}
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const workerPath = path.join(currentDir, 'isolated-vm-worker.cjs')
const candidatePaths = [
path.join(currentDir, 'isolated-vm-worker.cjs'),
path.join(process.cwd(), 'lib', 'execution', 'isolated-vm-worker.cjs'),
]
const workerPath = candidatePaths.find((p) => fs.existsSync(p))
if (!fs.existsSync(workerPath)) {
if (!workerPath) {
settleSpawnInProgress()
reject(new Error(`Worker file not found at ${workerPath}`))
reject(new Error(`Worker file not found at any of: ${candidatePaths.join(', ')}`))
return
}

View File

@@ -1,4 +1,4 @@
import { additionalPackages } from '@trigger.dev/build/extensions/core'
import { additionalFiles, additionalPackages } from '@trigger.dev/build/extensions/core'
import { defineConfig } from '@trigger.dev/sdk'
import { env } from './lib/core/config/env'
@@ -15,9 +15,11 @@ export default defineConfig({
},
dirs: ['./background'],
build: {
external: ['isolated-vm'],
extensions: [
additionalFiles({ files: ['./lib/execution/isolated-vm-worker.cjs'] }),
additionalPackages({
packages: ['unpdf', 'pdf-lib'],
packages: ['unpdf', 'pdf-lib', 'isolated-vm'],
}),
],
},