working checkbox, need to add zapier oauth2

This commit is contained in:
waleed
2025-12-09 14:13:50 -08:00
parent ce72880935
commit 4d9ae94047
3 changed files with 430 additions and 75 deletions

View File

@@ -19,10 +19,15 @@ export const ZapierBlock: BlockConfig<ZapierResponse> = {
type: 'dropdown',
options: [
{ label: 'Execute Action', id: 'execute' },
{ label: 'Stateless Execute', id: 'stateless_execute' },
{ label: 'List Actions', id: 'list' },
{ label: 'Search Apps', id: 'search_apps' },
{ label: 'Search App Actions', id: 'search_app_actions' },
{ label: 'Find Actions', id: 'guess' },
{ label: 'Get Action Details', id: 'get_action_details' },
{ label: 'Create Action', id: 'create' },
{ label: 'Update Action', id: 'update' },
{ label: 'Delete Action', id: 'delete' },
],
value: () => 'execute',
},
@@ -111,9 +116,9 @@ export const ZapierBlock: BlockConfig<ZapierResponse> = {
title: 'Action Types',
type: 'checkbox-list',
options: [
{ label: 'Write (Create/Send)', id: 'write' },
{ label: 'Search (Find)', id: 'search' },
{ label: 'Read (Get)', id: 'read' },
{ label: 'Write (Create/Send)', id: 'actionTypes_write' },
{ label: 'Search (Find)', id: 'actionTypes_search' },
{ label: 'Read (Get)', id: 'actionTypes_read' },
],
condition: {
field: 'operation',
@@ -178,6 +183,266 @@ export const ZapierBlock: BlockConfig<ZapierResponse> = {
value: 'create',
},
},
// Stateless Execute fields
{
id: 'statelessApp',
title: 'App',
type: 'short-input',
placeholder: 'App identifier (e.g., "SlackAPI", "GmailV2API")',
condition: {
field: 'operation',
value: 'stateless_execute',
},
required: true,
},
{
id: 'statelessAction',
title: 'Action',
type: 'short-input',
placeholder: 'Action identifier (e.g., "send_channel_message")',
condition: {
field: 'operation',
value: 'stateless_execute',
},
required: true,
},
{
id: 'statelessInstructions',
title: 'Instructions',
type: 'long-input',
placeholder: 'Describe what you want to do in plain English',
condition: {
field: 'operation',
value: 'stateless_execute',
},
required: true,
},
{
id: 'statelessActionType',
title: 'Action Type',
type: 'dropdown',
options: [
{ label: 'Write', id: 'write' },
{ label: 'Search', id: 'search' },
{ label: 'Read', id: 'read' },
],
value: () => 'write',
condition: {
field: 'operation',
value: 'stateless_execute',
},
},
{
id: 'statelessParams',
title: 'Parameters',
type: 'code',
placeholder: '{\n "channel": {"mode": "locked", "value": "#general"}\n}',
condition: {
field: 'operation',
value: 'stateless_execute',
},
},
{
id: 'statelessPreviewOnly',
title: 'Preview Mode',
type: 'dropdown',
options: [
{ label: 'Execute', id: 'false' },
{ label: 'Preview Only', id: 'true' },
],
value: () => 'false',
condition: {
field: 'operation',
value: 'stateless_execute',
},
},
// Search App Actions fields
{
id: 'searchAppActionsApp',
title: 'App',
type: 'short-input',
placeholder: 'App identifier (e.g., "SlackAPI", "GmailV2API")',
condition: {
field: 'operation',
value: 'search_app_actions',
},
required: true,
},
{
id: 'searchAppActionsQuery',
title: 'Search Query',
type: 'short-input',
placeholder: 'Optional: filter actions by name',
condition: {
field: 'operation',
value: 'search_app_actions',
},
},
{
id: 'searchAppActionsTypes',
title: 'Action Types',
type: 'checkbox-list',
options: [
{ label: 'Write (Create/Send)', id: 'searchAppActionsTypes_write' },
{ label: 'Search (Find)', id: 'searchAppActionsTypes_search' },
{ label: 'Read (Get)', id: 'searchAppActionsTypes_read' },
],
condition: {
field: 'operation',
value: 'search_app_actions',
},
},
// Get Action Details fields
{
id: 'detailsApp',
title: 'App',
type: 'short-input',
placeholder: 'App identifier (e.g., "SlackAPI", "GmailV2API")',
condition: {
field: 'operation',
value: 'get_action_details',
},
required: true,
},
{
id: 'detailsAction',
title: 'Action',
type: 'short-input',
placeholder: 'Action identifier (e.g., "send_channel_message")',
condition: {
field: 'operation',
value: 'get_action_details',
},
required: true,
},
{
id: 'detailsActionType',
title: 'Action Type',
type: 'dropdown',
options: [
{ label: 'Write', id: 'write' },
{ label: 'Search', id: 'search' },
{ label: 'Read', id: 'read' },
],
value: () => 'write',
condition: {
field: 'operation',
value: 'get_action_details',
},
},
{
id: 'includeNeeds',
title: 'Include Inputs (Needs)',
type: 'dropdown',
options: [
{ label: 'Yes', id: 'true' },
{ label: 'No', id: 'false' },
],
value: () => 'true',
condition: {
field: 'operation',
value: 'get_action_details',
},
},
{
id: 'includeGives',
title: 'Include Outputs (Gives)',
type: 'dropdown',
options: [
{ label: 'Yes', id: 'true' },
{ label: 'No', id: 'false' },
],
value: () => 'false',
condition: {
field: 'operation',
value: 'get_action_details',
},
},
{
id: 'includeSample',
title: 'Include Sample',
type: 'dropdown',
options: [
{ label: 'Yes', id: 'true' },
{ label: 'No', id: 'false' },
],
value: () => 'false',
condition: {
field: 'operation',
value: 'get_action_details',
},
},
// Update Action fields
{
id: 'updateActionId',
title: 'Action ID',
type: 'short-input',
placeholder: 'The ID of the AI Action to update',
condition: {
field: 'operation',
value: 'update',
},
required: true,
},
{
id: 'updateApp',
title: 'App',
type: 'short-input',
placeholder: 'App identifier (e.g., "SlackAPI")',
condition: {
field: 'operation',
value: 'update',
},
required: true,
},
{
id: 'updateAction',
title: 'Action',
type: 'short-input',
placeholder: 'Action identifier (e.g., "send_channel_message")',
condition: {
field: 'operation',
value: 'update',
},
required: true,
},
{
id: 'updateActionType',
title: 'Action Type',
type: 'dropdown',
options: [
{ label: 'Write', id: 'write' },
{ label: 'Search', id: 'search' },
{ label: 'Read', id: 'read' },
],
value: () => 'write',
condition: {
field: 'operation',
value: 'update',
},
},
{
id: 'updateParams',
title: 'Parameters',
type: 'code',
placeholder: '{\n "channel": "#general"\n}',
condition: {
field: 'operation',
value: 'update',
},
},
// Delete Action fields
{
id: 'deleteActionId',
title: 'Action ID',
type: 'short-input',
placeholder: 'The ID of the AI Action to delete',
condition: {
field: 'operation',
value: 'delete',
},
required: true,
},
],
tools: {
access: [
@@ -197,14 +462,24 @@ export const ZapierBlock: BlockConfig<ZapierResponse> = {
switch (params.operation) {
case 'execute':
return 'zapier_execute_action'
case 'stateless_execute':
return 'zapier_stateless_execute'
case 'list':
return 'zapier_list_actions'
case 'search_apps':
return 'zapier_search_apps'
case 'search_app_actions':
return 'zapier_search_app_actions'
case 'guess':
return 'zapier_guess_actions'
case 'get_action_details':
return 'zapier_get_action_details'
case 'create':
return 'zapier_create_action'
case 'update':
return 'zapier_update_action'
case 'delete':
return 'zapier_delete_action'
default:
throw new Error(`Invalid Zapier operation: ${params.operation}`)
}
@@ -224,88 +499,121 @@ export const ZapierBlock: BlockConfig<ZapierResponse> = {
action,
createActionType,
createParams,
statelessApp,
statelessAction,
statelessInstructions,
statelessActionType,
statelessParams,
statelessPreviewOnly,
searchAppActionsApp,
searchAppActionsQuery,
detailsApp,
detailsAction,
detailsActionType,
includeNeeds,
includeGives,
includeSample,
updateActionId,
updateApp,
updateAction,
updateActionType,
updateParams,
deleteActionId,
} = params
if (!apiKey) {
throw new Error('Zapier API key is required')
const baseParams: Record<string, any> = { apiKey }
// Helper to parse JSON params
const parseJsonParams = (jsonParams: any) => {
if (!jsonParams) return undefined
try {
return typeof jsonParams === 'string' ? JSON.parse(jsonParams) : jsonParams
} catch {
throw new Error('Invalid JSON in parameters field')
}
}
const baseParams: Record<string, any> = {
apiKey,
// Helper to collect checkbox-list values
// Use truthy check since values may be boolean true or string "true" after serialization
const collectActionTypes = (prefix: string) => {
const types: string[] = []
const writeVal = params[`${prefix}_write`]
const searchVal = params[`${prefix}_search`]
const readVal = params[`${prefix}_read`]
if (writeVal === true || writeVal === 'true') types.push('write')
if (searchVal === true || searchVal === 'true') types.push('search')
if (readVal === true || readVal === 'true') types.push('read')
return types.length > 0 ? types : undefined
}
switch (operation) {
case 'execute': {
if (!actionId) {
throw new Error('Action ID is required for execute operation')
}
if (!instructions) {
throw new Error('Instructions are required for execute operation')
}
case 'execute':
baseParams.actionId = actionId
baseParams.instructions = instructions
if (execParams) {
try {
baseParams.params =
typeof execParams === 'string' ? JSON.parse(execParams) : execParams
} catch {
throw new Error('Invalid JSON in parameters field')
}
}
baseParams.params = parseJsonParams(execParams)
baseParams.previewOnly = previewOnly === 'true'
break
}
case 'stateless_execute':
baseParams.app = statelessApp
baseParams.action = statelessAction
baseParams.instructions = statelessInstructions
baseParams.actionType = statelessActionType || 'write'
baseParams.params = parseJsonParams(statelessParams)
baseParams.previewOnly = statelessPreviewOnly === 'true'
break
case 'list':
break
case 'search_apps':
if (searchQuery) {
baseParams.query = searchQuery
}
if (searchQuery) baseParams.query = searchQuery
break
case 'search_app_actions':
baseParams.app = searchAppActionsApp
if (searchAppActionsQuery) baseParams.query = searchAppActionsQuery
baseParams.actionTypes = collectActionTypes('searchAppActionsTypes')
break
case 'guess': {
if (!guessQuery) {
throw new Error('Search query is required for find actions operation')
}
baseParams.query = guessQuery
const actionTypes: string[] = []
if (params.write === true) actionTypes.push('write')
if (params.search === true) actionTypes.push('search')
if (params.read === true) actionTypes.push('read')
if (actionTypes.length > 0) {
baseParams.actionTypes = actionTypes
}
// Checkbox-list values are stored under prefixed option IDs (actionTypes_write, etc.)
baseParams.actionTypes = collectActionTypes('actionTypes')
if (resultCount) {
const count = Number.parseInt(resultCount, 10)
if (!Number.isNaN(count)) {
baseParams.count = count
}
if (!Number.isNaN(count)) baseParams.count = count
}
break
}
case 'create': {
if (!app) {
throw new Error('App is required for create action operation')
}
if (!action) {
throw new Error('Action is required for create action operation')
}
case 'get_action_details':
baseParams.app = detailsApp
baseParams.action = detailsAction
baseParams.actionType = detailsActionType || 'write'
baseParams.includeNeeds = includeNeeds !== 'false'
baseParams.includeGives = includeGives === 'true'
baseParams.includeSample = includeSample === 'true'
break
case 'create':
baseParams.app = app
baseParams.action = action
baseParams.actionType = createActionType || 'write'
if (createParams) {
try {
baseParams.params =
typeof createParams === 'string' ? JSON.parse(createParams) : createParams
} catch {
throw new Error('Invalid JSON in parameters field')
}
}
baseParams.params = parseJsonParams(createParams)
break
case 'update':
baseParams.actionId = updateActionId
baseParams.app = updateApp
baseParams.action = updateAction
baseParams.actionType = updateActionType || 'write'
baseParams.params = parseJsonParams(updateParams)
break
case 'delete':
baseParams.actionId = deleteActionId
break
}
}
return baseParams
@@ -320,19 +628,47 @@ export const ZapierBlock: BlockConfig<ZapierResponse> = {
instructions: { type: 'string', description: 'Plain English instructions for the action' },
params: { type: 'json', description: 'Optional parameter constraints' },
previewOnly: { type: 'string', description: 'Whether to preview without executing' },
// Stateless execute inputs
statelessApp: { type: 'string', description: 'App identifier for stateless execute' },
statelessAction: { type: 'string', description: 'Action identifier for stateless execute' },
statelessInstructions: { type: 'string', description: 'Instructions for stateless execute' },
statelessActionType: { type: 'string', description: 'Action type for stateless execute' },
statelessParams: { type: 'json', description: 'Parameters for stateless execute' },
statelessPreviewOnly: { type: 'string', description: 'Preview mode for stateless execute' },
// Search inputs
searchQuery: { type: 'string', description: 'App search query' },
// Search app actions inputs
searchAppActionsApp: { type: 'string', description: 'App to search actions for' },
searchAppActionsQuery: { type: 'string', description: 'Query to filter actions' },
searchAppActionsTypes_write: { type: 'boolean', description: 'Include write actions' },
searchAppActionsTypes_search: { type: 'boolean', description: 'Include search actions' },
searchAppActionsTypes_read: { type: 'boolean', description: 'Include read actions' },
// Guess inputs
guessQuery: { type: 'string', description: 'Natural language query to find actions' },
write: { type: 'boolean', description: 'Include write actions' },
search: { type: 'boolean', description: 'Include search actions' },
read: { type: 'boolean', description: 'Include read actions' },
actionTypes_write: { type: 'boolean', description: 'Include write actions' },
actionTypes_search: { type: 'boolean', description: 'Include search actions' },
actionTypes_read: { type: 'boolean', description: 'Include read actions' },
resultCount: { type: 'string', description: 'Maximum number of results' },
// Get action details inputs
detailsApp: { type: 'string', description: 'App identifier for action details' },
detailsAction: { type: 'string', description: 'Action identifier for action details' },
detailsActionType: { type: 'string', description: 'Action type for action details' },
includeNeeds: { type: 'string', description: 'Include input requirements' },
includeGives: { type: 'string', description: 'Include output specifications' },
includeSample: { type: 'string', description: 'Include sample data' },
// Create inputs
app: { type: 'string', description: 'App identifier' },
action: { type: 'string', description: 'Action identifier' },
createActionType: { type: 'string', description: 'Type of action to create' },
createParams: { type: 'json', description: 'Pre-configured parameters' },
// Update inputs
updateActionId: { type: 'string', description: 'AI Action ID to update' },
updateApp: { type: 'string', description: 'App identifier for update' },
updateAction: { type: 'string', description: 'Action identifier for update' },
updateActionType: { type: 'string', description: 'Action type for update' },
updateParams: { type: 'json', description: 'Parameters for update' },
// Delete inputs
deleteActionId: { type: 'string', description: 'AI Action ID to delete' },
},
outputs: {
// Execute Action outputs
@@ -439,5 +775,31 @@ export const ZapierBlock: BlockConfig<ZapierResponse> = {
type: 'number',
description: 'Authentication ID used for the app',
},
// Get Action Details outputs
needs: {
type: 'json',
description: 'Array of input requirements for the action',
},
gives: {
type: 'json',
description: 'Array of output fields from the action',
},
sample: {
type: 'json',
description: 'Sample execution result',
},
customNeedsProbability: {
type: 'number',
description: 'Probability that action has custom/dynamic input fields',
},
// Delete Action outputs
deleted: {
type: 'boolean',
description: 'Whether the action was successfully deleted',
},
message: {
type: 'string',
description: 'Status message for delete operation',
},
},
}

View File

@@ -115,10 +115,6 @@ export class Serializer {
safeParallels
)
if (validateRequired) {
this.validateSubflowsBeforeExecution(blocks, safeLoops, safeParallels)
}
return {
version: '1.0',
blocks: Object.values(blocks).map((block) =>
@@ -139,18 +135,6 @@ export class Serializer {
}
}
/**
* Validate loop and parallel subflows for required inputs when running in "each/collection" modes
*/
private validateSubflowsBeforeExecution(
blocks: Record<string, BlockState>,
loops: Record<string, Loop>,
parallels: Record<string, Parallel>
): void {
// Note: Empty collections in forEach loops and parallel collection mode are handled gracefully
// at runtime - the loop/parallel will simply be skipped. No build-time validation needed.
}
private serializeBlock(
block: BlockState,
options: {
@@ -367,6 +351,15 @@ export class Serializer {
) {
params[id] = subBlock.value
}
if (subBlockConfig?.type === 'checkbox-list' && Array.isArray(subBlockConfig.options)) {
subBlockConfig.options.forEach((option: { id: string; label: string }) => {
const optionSubBlock = block.subBlocks[option.id]
if (optionSubBlock !== undefined) {
params[option.id] = optionSubBlock.value
}
})
}
})
// Then check for any subBlocks with default values

View File

@@ -49,7 +49,7 @@ export const zapierExecuteActionTool: ToolConfig<
request: {
url: (params) =>
`https://actions.zapier.com/api/v2/ai-actions/${encodeURIComponent(params.actionId)}/execute${params.previewOnly ? '?preview_only=true' : ''}`,
`https://actions.zapier.com/api/v2/ai-actions/${encodeURIComponent(params.actionId)}/execute/${params.previewOnly ? '?preview_only=true' : ''}`,
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',