create_campaign
Create email campaigns with a three-stage workflow that validates prerequisites, previews configurations, and executes creation based on provided parameters.
Instructions
Create a new email campaign with bulletproof three-stage workflow ensuring 100% success rate. Handles both simple requests ("create a campaign") and complex detailed specifications seamlessly.
INTELLIGENT WORKFLOW:
Simple Usage: Just provide basic info (name, subject, body) - tool automatically handles prerequisites
Advanced Usage: Specify all parameters for immediate creation
Guided Mode: Use stage parameter for step-by-step control
THREE-STAGE PROCESS:
Prerequisite Check (
stage: "prerequisite_check"): Validates accounts and collects missing required fieldsCampaign Preview (
stage: "preview"): Shows complete configuration for user confirmationValidated Creation (
stage: "create"): Creates campaign with fully validated parameters
AUTO-STAGE DETECTION: Tool automatically determines appropriate stage based on provided parameters for seamless user experience.
EXAMPLE USAGE:
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| body | No | Email body content. Use \n for line breaks - they will be automatically converted to HTML paragraphs for optimal visual rendering in Instantly. Double line breaks (\n\n) create new paragraphs, single line breaks (\n) become line breaks within paragraphs. Example: "Hi {{firstName}},\n\nI hope this email finds you well.\n\nBest regards,\nYour Name". Supports all Instantly personalization variables. Required for creation but can be collected during prerequisite check. | |
| confirm_creation | No | Explicit confirmation for campaign creation (optional). Required when stage is "create" or when tool shows preview. Set to true to confirm you want to proceed with campaign creation. | |
| continue_thread | No | Automatically blank follow-up email subjects for thread continuation (optional, default: false). When true, all follow-up emails (steps 2+) will have empty subjects to maintain email thread continuity. Only applies when sequence_subjects is not provided. If sequence_subjects is provided, this parameter is ignored. | |
| daily_limit | No | Maximum emails to send per day across all sending accounts (optional, default: 50). Higher limits may affect deliverability. Recommended: 20-100 for new accounts, up to 500 for warmed accounts. | |
| days | No | Days of the week to send emails (optional, default: Monday-Friday only). Specify which days the campaign should send emails. Weekend sending is disabled by default for better deliverability. | |
| email_gap_minutes | No | Minutes to wait between individual emails (optional, default: 10). Longer gaps improve deliverability. Minimum 1 minute, maximum 1440 minutes (24 hours). | |
| email_list | No | Array of verified sending account email addresses. Must be exact addresses from your Instantly workspace. If not provided, tool will auto-discover and suggest eligible accounts during prerequisite check. | |
| link_tracking | No | Track link clicks in emails (optional, default: false). When enabled, links are replaced with tracking URLs. | |
| message | No | Shortcut parameter: single string containing both subject and body. First sentence becomes subject, remainder becomes body. Alternative to separate subject/body parameters. | |
| name | No | Campaign name. Choose a descriptive name that identifies the campaign purpose. Required for campaign creation but can be collected during prerequisite check if missing. | |
| open_tracking | No | Track email opens (optional, default: false). When enabled, invisible tracking pixels are added to emails. | |
| schedule_name | No | Schedule name (optional, default: "Default Schedule"). Internal name for the sending schedule. | |
| sequence_bodies | No | Optional array of body content for each sequence step. If provided, must contain at least as many items as sequence_steps. Each string will be used as the body for the corresponding step (index 0 = first email, index 1 = first follow-up, etc.). If not provided, the main "body" parameter will be duplicated across all steps with automatic follow-up prefixes. Use \n for line breaks - they will be automatically converted to HTML paragraphs. | |
| sequence_steps | No | Number of steps in the email sequence (optional, default: 1 for just the initial email). Each step creates an email with the required API v2 structure: sequences[0].steps[i] containing type="email", delay (days before sending), and variants[] array with subject, body, and v_disabled fields. If set to 2 or more, additional follow-up emails are created automatically. Maximum 10 steps. | |
| sequence_subjects | No | Optional array of subject lines for each sequence step. If provided, must contain at least as many items as sequence_steps. Each string will be used as the subject for the corresponding step. Use empty strings ("") for follow-up emails to maintain thread continuity. If not provided, the main "subject" parameter will be used for the first email, and follow-ups will get "Follow-up X:" prefixes. | |
| stage | No | Workflow stage control (optional). "prerequisite_check": Validate accounts and collect missing fields. "preview": Show complete campaign configuration for confirmation. "create": Execute campaign creation. If not specified, tool auto-detects appropriate stage based on provided parameters. | |
| step_delay_days | No | Days to wait before sending each follow-up email (optional, default: 3 days). This sets the delay field in sequences[0].steps[i].delay as required by the API. Each follow-up step will have this delay value. Minimum 1 day, maximum 30 days. | |
| stop_on_auto_reply | No | Stop sending when auto-reply is detected (optional, default: true). Helps avoid sending to out-of-office or vacation responders. | |
| stop_on_reply | No | Stop sending follow-ups when lead replies (optional, default: true). Recommended to keep true to avoid annoying engaged prospects. | |
| subject | No | Email subject line. Supports personalization variables like {{firstName}}, {{lastName}}, {{companyName}}. Example: "Quick question about {{companyName}}". Required for creation but can be collected during prerequisite check. | |
| text_only | No | Send as text-only emails (optional, default: false for HTML). Text-only emails often have better deliverability but no formatting. | |
| timezone | No | Timezone for campaign schedule (optional, default: "America/Chicago"). All timing_from and timing_to values will be interpreted in this timezone. | |
| timing_from | No | Daily start time in HH:MM format (optional, default: "09:00"). Emails will only be sent after this time each day. Example: "09:00" for 9 AM. | |
| timing_to | No | Daily end time in HH:MM format (optional, default: "17:00"). Emails will stop being sent after this time each day. Example: "17:00" for 5 PM. |
Implementation Reference
- src/handlers/tool-executor.ts:357-553 (handler)Primary handler for executing the create_campaign tool. Implements two-step workflow: 1) discovers eligible sender accounts if email_list missing, 2) validates parameters, builds API v2 payload, and creates campaign via POST /campaigns.case 'create_campaign': { console.error('[Instantly MCP] 🚀 Executing create_campaign with automatic account discovery...'); try { // STEP 0: Automatic Account Discovery - Fetch and display eligible accounts console.error('[Instantly MCP] 📋 Fetching eligible sender accounts...'); // Check if validation should be skipped (used throughout this handler) const skipValidation = process.env.SKIP_ACCOUNT_VALIDATION === 'true'; const isTestKey = apiKey?.includes('test') || apiKey?.includes('demo'); if (!skipValidation && !isTestKey) { const accountsResult = await getAllAccounts(apiKey); const accounts = accountsResult.data || accountsResult; if (!accounts || !Array.isArray(accounts) || accounts.length === 0) { throw new McpError( ErrorCode.InvalidParams, '❌ No accounts found in your workspace.\n\n' + '📋 Required Action:\n' + '1. Go to your Instantly.ai dashboard\n' + '2. Navigate to Accounts section\n' + '3. Add and verify email accounts\n' + '4. Complete warmup process for each account\n' + '5. Then retry campaign creation' ); } // Filter for eligible accounts (active, setup complete, warmup complete) const eligibleAccounts = accounts.filter(account => account.status === 1 && !account.setup_pending && account.warmup_status === 1 ); if (eligibleAccounts.length === 0) { const accountIssues = accounts.slice(0, 10).map(acc => ({ email: acc.email, issues: [ ...(acc.status !== 1 ? ['❌ Account not active'] : []), ...(acc.setup_pending ? ['⏳ Setup pending'] : []), ...(acc.warmup_status !== 1 ? ['🔥 Warmup not complete'] : []) ] })); throw new McpError( ErrorCode.InvalidParams, `❌ No eligible sender accounts found for campaign creation.\n\n` + `📊 Account Status (showing first 10 of ${accounts.length} total):\n${ accountIssues.map(acc => `• ${acc.email}: ${acc.issues.join(', ')}`).join('\n') }\n\n` + `✅ Requirements for eligible accounts:\n` + `• Account must be active (status = 1)\n` + `• Setup must be complete (no pending setup)\n` + `• Warmup must be complete (warmup_status = 1)\n\n` + `📋 Next Steps:\n` + `1. Complete setup for pending accounts\n` + `2. Wait for warmup to complete\n` + `3. Ensure accounts are active\n` + `4. Then retry campaign creation` ); } // If email_list is NOT provided, return eligible accounts and ask user to select if (!args.email_list || args.email_list.length === 0) { const eligibleEmailsList = eligibleAccounts.map(acc => ({ email: acc.email, warmup_score: acc.warmup_score || 0, status: 'ready' })); return { content: [ { type: 'text', text: JSON.stringify({ success: false, stage: 'account_selection_required', message: '📋 Eligible Sender Accounts Found', total_eligible_accounts: eligibleAccounts.length, total_accounts: accounts.length, eligible_accounts: eligibleEmailsList, instructions: [ `✅ Found ${eligibleAccounts.length} eligible sender accounts (out of ${accounts.length} total)`, '', '📧 Eligible Sender Accounts:', ...eligibleEmailsList.map(acc => ` • ${acc.email} (warmup score: ${acc.warmup_score})`), '', '❓ How many of these accounts would you like to use as senders for this campaign?', '', '💡 Instantly.ai\'s core value is multi-account sending for better deliverability.', ' Most users use 10-100+ accounts per campaign.', '', '📝 Next Step:', ' Call create_campaign again with the email_list parameter containing the sender emails you want to use.', '', ' Example:', ` email_list: ["${eligibleEmailsList[0]?.email || 'email1@domain.com'}", "${eligibleEmailsList[1]?.email || 'email2@domain.com'}"]` ].join('\n'), required_action: { step: 'select_sender_accounts', description: 'Select which eligible accounts to use as senders', parameter: 'email_list', example: eligibleEmailsList.slice(0, 3).map(acc => acc.email) } }, null, 2) } ] }; } console.error(`[Instantly MCP] ✅ Found ${eligibleAccounts.length} eligible accounts, proceeding with validation...`); } // Step 1: Clean up and validate parameters for API compatibility console.error('[Instantly MCP] 🧹 Cleaning up parameters for API compatibility...'); const { cleanedArgs, warnings } = cleanupAndValidateParameters(args); if (warnings.length > 0) { console.error('[Instantly MCP] ⚠️ Parameter cleanup warnings:'); warnings.forEach(warning => console.error(` ${warning}`)); } // Step 2: Apply smart defaults and enhancements console.error('[Instantly MCP] 🔧 Applying smart defaults...'); const smartDefaultsResult = await applySmartDefaults(cleanedArgs); const enhanced_args = smartDefaultsResult.enhanced_args; // Step 3: Validate the enhanced arguments console.error('[Instantly MCP] ✅ Validating enhanced campaign data...'); // WORKAROUND: Add temporary subject/body for complex campaigns to pass validation const hasComplexStructure = enhanced_args.campaign_schedule && enhanced_args.sequences; const validationArgs = { ...enhanced_args }; if (hasComplexStructure && !validationArgs.subject && !validationArgs.body) { validationArgs.subject = 'temp-subject-for-validation'; validationArgs.body = 'temp-body-for-validation'; } const validatedData = await validateCampaignData(validationArgs); // Step 4: Validate sender email addresses against accounts (skip for test API keys or if disabled) // Note: skipValidation and isTestKey are already declared at the top of this handler if (!skipValidation && !isTestKey && enhanced_args.email_list && enhanced_args.email_list.length > 0) { console.error('[Instantly MCP] 🔍 Validating sender email addresses...'); await validateEmailListAgainstAccounts(enhanced_args.email_list, apiKey); } else { console.error('[Instantly MCP] ⏭️ Skipping account validation (test key or disabled)'); } // Step 5: Build the API v2 compliant payload console.error('[Instantly MCP] 🏗️ Building API v2 compliant payload...'); const campaignPayload = buildCampaignPayload(enhanced_args); console.error('[Instantly MCP] 📦 Generated payload:', JSON.stringify(campaignPayload, null, 2)); // Step 6: Make the API request console.error('[Instantly MCP] 🌐 Making API request to create campaign...'); const response = await fetch('https://api.instantly.ai/api/v2/campaigns', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify(campaignPayload) }); const responseText = await response.text(); console.error(`[Instantly MCP] 📡 API Response Status: ${response.status}`); console.error(`[Instantly MCP] 📡 API Response Body: ${responseText}`); if (!response.ok) { throw new McpError(ErrorCode.InternalError, `Campaign creation failed (${response.status}): ${responseText}`); } const result = JSON.parse(responseText); return { content: [ { type: 'text', text: JSON.stringify({ success: true, campaign: result, message: 'Campaign created successfully with API v2 compliant payload', payload_used: campaignPayload }, null, 2) } ] }; } catch (error: any) { console.error('[Instantly MCP] ❌ create_campaign error:', error); throw error; } }
- src/tools/campaign-tools.ts:12-40 (registration)MCP tool registration object defining name, title, description, annotations, and inputSchema for the create_campaign tool.{ name: 'create_campaign', title: 'Create Campaign', description: 'Create email campaign. Two-step: 1) Call with name/subject/body to discover accounts, 2) Call again with email_list. Use sequence_steps for multi-step sequences.', annotations: { destructiveHint: false }, inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Campaign name' }, subject: { type: 'string', description: 'Subject (<50 chars). Personalization: {{firstName}}, {{companyName}}' }, body: { type: 'string', description: 'Email body (\\n for line breaks). Personalization: {{firstName}}, {{lastName}}, {{companyName}}' }, email_list: { type: 'array', items: { type: 'string' }, description: 'Sender emails (from Step 1 eligible list)' }, track_opens: { type: 'boolean', default: false }, track_clicks: { type: 'boolean', default: false }, timezone: { type: 'string', default: DEFAULT_TIMEZONE, description: `Supported: ${BUSINESS_PRIORITY_TIMEZONES.slice(0, 5).join(', ')}...` }, timing_from: { type: 'string', default: '09:00', description: '24h format' }, timing_to: { type: 'string', default: '17:00', description: '24h format' }, daily_limit: { type: 'number', default: 30, description: 'Emails/day/account (max 30)' }, email_gap: { type: 'number', default: 10, description: 'Minutes between emails (1-1440)' }, stop_on_reply: { type: 'boolean', default: true }, stop_on_auto_reply: { type: 'boolean', default: true }, sequence_steps: { type: 'number', default: 1, description: 'Steps in sequence (1-10)' }, step_delay_days: { type: 'number', default: 3, description: 'Days between steps (1-30)' }, sequence_subjects: { type: 'array', items: { type: 'string' }, description: 'Custom subjects per step' }, sequence_bodies: { type: 'array', items: { type: 'string' }, description: 'Custom bodies per step' } }, required: ['name', 'subject', 'body'] } },
- src/validation.ts:151-271 (schema)Comprehensive Zod v4 schema for validating create_campaign inputs, including required fields (name, subject, body, email_list), optional scheduling/tracking params, and refinements for compliance and best practices.export const CreateCampaignSchema = z.object({ // Workflow control stage: CampaignStageSchema.optional(), confirm_creation: z.boolean().optional(), // Required campaign fields with enhanced guidance name: z.string() .min(1, 'Campaign name is required. Provide a descriptive name like "Q4 Product Launch Campaign" or "Holiday Sales Outreach"') .max(255, 'Campaign name cannot exceed 255 characters. Keep it concise but descriptive.'), subject: z.string() .min(1, 'Email subject line is required. This is what recipients see in their inbox.') .max(255, 'Subject line cannot exceed 255 characters. For better deliverability, keep it under 50 characters.') .refine( (val) => val.length <= 50, { message: 'subject: Subject line is over 50 characters. Shorter subjects have better open rates. Consider: "{{firstName}}, quick question about {{companyName}}" • "Helping {{companyName}} with [specific problem]" • "{{firstName}}, saw your recent [achievement/news]"' } ) .optional(), // Made optional for complex campaigns body: z.string() .min(1, 'Email body cannot be empty') .refine( (val) => typeof val === 'string', 'Body must be a plain string, not an object or array' ) .refine( (val) => !val.includes('\\"') && !val.includes('\\t') && !val.includes('\\r'), 'Body contains escaped characters. Use actual \\n characters, not escaped JSON' ) .refine( (val) => { // Check for potentially problematic HTML tags (allow <p>, <br>, <br/> for formatting) if (val && val.includes('<') && val.includes('>')) { // Allow specific formatting tags that are safe and enhance visual rendering const allowedTags = /<\/?(?:p|br|br\/)>/gi; const bodyWithoutAllowedTags = val.replace(allowedTags, ''); // Check if there are any remaining HTML tags after removing allowed ones if (bodyWithoutAllowedTags.includes('<') && bodyWithoutAllowedTags.includes('>')) { return false; } } return true; }, 'Body contains unsupported HTML tags. Only <p>, <br>, and <br/> tags are allowed for formatting. Use plain text with \\n for line breaks. Example: "Hi {{firstName}},\\n\\nYour message here."' ) .optional(), // Made optional for complex campaigns email_list: z.array(EmailSchema) .min(1, 'At least one sender email address is required. ⚠️ CRITICAL: You MUST call list_accounts first to get verified email addresses. NEVER use placeholder emails like test@example.com or user@example.com.') .max(100, 'Cannot specify more than 100 email addresses in a single campaign. Consider creating multiple campaigns for larger lists.') .refine( (emails) => { // Check for common placeholder/fake email patterns const placeholderPatterns = [ /test@/i, /example\.com$/i, /user@/i, /email@/i, /demo@/i, /sample@/i ]; const hasPlaceholder = emails.some(email => placeholderPatterns.some(pattern => pattern.test(email)) ); return !hasPlaceholder; }, { message: '⚠️ CRITICAL ERROR: Placeholder/fake email addresses detected (e.g., test@example.com, user@example.com). You MUST:\n1. Call list_accounts first to get real, verified email addresses\n2. Use ONLY emails from the list_accounts response\n3. NEVER use placeholder or example email addresses\n\nIt seems the email account used in the test is invalid for creating campaigns. Here are some of your available eligible sender accounts you can use for the campaign:\n\nPlease call list_accounts to see your verified email addresses, then use those addresses in the email_list parameter.' } ), // Optional scheduling parameters timezone: TimezoneSchema.optional(), timing_from: TimeFormatSchema.optional(), timing_to: TimeFormatSchema.optional(), days: DaysConfigSchema, // Optional campaign settings - EXACT parameters from Instantly.ai API v2 daily_limit: z.number().int().min(1).max(30).optional(), email_gap: z.number().int().min(1).max(1440).optional(), // API v2 parameter name // Complex campaign structure campaign_schedule: z.object({ schedules: z.array(z.object({ days: z.record(z.string(), z.boolean()), from: z.string(), to: z.string(), timezone: TimezoneSchema })) }).optional(), sequences: z.array(z.object({ steps: z.array(z.object({ subject: z.string(), body: z.string().min(1, 'Email body cannot be empty for any step'), delay: z.number().int().min(0) })).min(1, 'At least one step is required in sequence') })).optional(), // Tracking settings open_tracking: z.boolean().optional(), link_tracking: z.boolean().optional(), stop_on_reply: z.boolean().optional() }).refine( (data) => { const hasSimpleParams = data.subject && data.body; const hasComplexStructure = data.campaign_schedule && data.sequences; return hasSimpleParams || hasComplexStructure; }, { message: 'Either provide simple parameters (subject, body, email_list) OR complex structure (campaign_schedule, sequences)', path: ['campaign_structure'] } );
- Supporting function for campaign prerequisite gathering: fetches accounts, determines eligibility, analyzes inputs, validates email_list, and provides detailed guidance for successful campaign creation.export async function gatherCampaignPrerequisites(args: any, apiKey?: string): Promise<any> { console.error('[Instantly MCP] 🔍 Gathering campaign prerequisites...'); // Step 1: Fetch and analyze available accounts let accounts: any[] = []; let eligibleAccounts: any[] = []; let accountAnalysis: any = {}; try { const accountsResult = await getAllAccounts(apiKey); // FIX: getAllAccounts returns { data: [...], metadata: {...} }, not an array directly accounts = accountsResult.data || accountsResult; if (!accounts || !Array.isArray(accounts) || accounts.length === 0) { return { stage: 'prerequisite_check', status: 'no_accounts_found', error: 'No accounts found in your workspace', required_action: { step: 1, action: 'add_accounts', description: 'You need to add at least one email account before creating campaigns', instructions: [ 'Go to your Instantly dashboard', 'Navigate to Accounts section', 'Add and verify email accounts', 'Complete warmup process for each account', 'Then retry campaign creation' ] } }; } // Analyze account eligibility eligibleAccounts = accounts.filter(account => account.status === 1 && !account.setup_pending && account.warmup_status === 1 ); const ineligibleAccounts = accounts.filter(account => account.status !== 1 || account.setup_pending || account.warmup_status !== 1 ); accountAnalysis = { total_accounts: accounts.length, eligible_accounts: eligibleAccounts.length, ineligible_accounts: ineligibleAccounts.length, eligible_emails: eligibleAccounts.map(acc => ({ email: acc.email, warmup_score: acc.warmup_score || 0, status: 'ready' })), ineligible_emails: ineligibleAccounts.map(acc => ({ email: acc.email, issues: [ ...(acc.status !== 1 ? ['Account not active'] : []), ...(acc.setup_pending ? ['Setup pending'] : []), ...(acc.warmup_status !== 1 ? ['Warmup not complete'] : []) ] })) }; } catch (error: any) { return { stage: 'prerequisite_check', status: 'account_fetch_failed', error: `Failed to fetch accounts: ${error.message}`, suggestion: 'Please check your API key and try again, or call list_accounts directly to troubleshoot' }; } // Step 2: Analyze provided campaign data const providedFields: Record<string, any> = { name: args?.name || null, subject: args?.subject || null, body: args?.body || null, email_list: args?.email_list || null, campaign_schedule: args?.campaign_schedule || null, sequences: args?.sequences || null }; // Determine required fields based on campaign type const hasComplexStructure = args?.campaign_schedule && args?.sequences; const requiredFields = hasComplexStructure ? ['name', 'email_list'] // Complex campaigns only need name and email_list : ['name', 'subject', 'body', 'email_list']; // Simple campaigns need all fields const missingFields = requiredFields.filter(field => !providedFields[field]); const hasAllRequired = missingFields.length === 0; // Step 3: Validate email_list if provided let emailValidation: any = null; if (args?.email_list && Array.isArray(args.email_list)) { const eligibleEmailSet = new Set(eligibleAccounts.map(acc => acc.email.toLowerCase())); const invalidEmails = args.email_list.filter((email: string) => !eligibleEmailSet.has(email.toLowerCase()) ); emailValidation = { provided_emails: args.email_list, valid_emails: args.email_list.filter((email: string) => eligibleEmailSet.has(email.toLowerCase()) ), invalid_emails: invalidEmails, validation_passed: invalidEmails.length === 0 }; } // Step 4: Generate comprehensive guidance const guidance = { next_steps: [] as any[], field_requirements: { name: { required: true, description: 'Campaign name for identification', example: 'Q4 Product Launch Campaign', provided: !!args?.name }, subject: { required: true, description: 'Email subject line', example: 'Introducing our new product line', formatting_tips: [ 'Keep under 50 characters for better deliverability', 'Avoid spam trigger words', 'Use personalization: {{firstName}} or {{companyName}}' ], provided: !!args?.subject }, body: { required: true, description: 'Email body content', formatting_requirements: [ 'Use plain text with \\n for line breaks', 'Personalization variables: {{firstName}}, {{lastName}}, {{companyName}}', 'Only <p>, <br>, and <br/> HTML tags are allowed', 'Avoid escaped characters like \\" or \\t' ], example: 'Hi {{firstName}},\\n\\nI hope this email finds you well.\\n\\nBest regards,\\nYour Name', provided: !!args?.body }, email_list: { required: true, description: 'Array of sender email addresses (must be from your verified accounts)', constraint: 'Only one sender email per campaign creation call', available_options: eligibleAccounts.map(acc => acc.email), example: ['john@yourcompany.com'], provided: !!args?.email_list, validation_status: emailValidation } }, optional_settings: { tracking: { track_opens: { default: false, description: 'Track when recipients open emails (disabled by default)', why_disabled: 'Email tracking can hurt deliverability and raises privacy concerns. Many email clients now block tracking pixels.' }, track_clicks: { default: false, description: 'Track when recipients click links (disabled by default)', why_disabled: 'Link tracking can trigger spam filters and reduces trust. Enable only if analytics are critical.' } }, scheduling: { timezone: { default: DEFAULT_TIMEZONE, description: 'Timezone for sending schedule (verified working timezone)' }, timing_from: { default: '09:00', description: 'Start time for sending (24h format)' }, timing_to: { default: '17:00', description: 'End time for sending (24h format)' }, days: { default: 'Monday-Friday', description: 'Days of week for sending', format: 'Object with boolean values for each day' } }, limits: { daily_limit: { default: 30, description: 'Maximum emails per day per account (30 for cold email compliance)', compliance_note: 'Higher limits may trigger spam filters and hurt deliverability. 30/day is the recommended maximum for cold outreach.' }, email_gap: { default: 10, description: 'Minutes between emails from same account (1-1440 minutes)' } } } }; // Add specific next steps based on current state if (eligibleAccounts.length === 0) { guidance.next_steps.push({ priority: 'critical', action: 'fix_account_issues', description: 'Resolve account eligibility issues before proceeding', details: accountAnalysis.ineligible_emails }); } else { guidance.next_steps.push({ priority: 'recommended', action: 'review_available_accounts', description: `You have ${eligibleAccounts.length} eligible sending accounts available`, accounts: accountAnalysis.eligible_emails }); } if (missingFields.length > 0) { guidance.next_steps.push({ priority: 'required', action: 'provide_missing_fields', description: `Provide the following required fields: ${missingFields.join(', ')}`, missing_fields: missingFields }); } if (emailValidation && !emailValidation.validation_passed) { guidance.next_steps.push({ priority: 'critical', action: 'fix_email_list', description: 'Email list contains invalid addresses', invalid_emails: emailValidation.invalid_emails, suggestion: 'Use only emails from your eligible accounts listed above' }); } // Include comprehensive guidance for users const fullGuidance = generateCampaignGuidance(); return { stage: 'prerequisite_check', status: hasAllRequired && eligibleAccounts.length > 0 && (!emailValidation || emailValidation.validation_passed) ? 'ready_for_creation' : 'missing_requirements', message: hasAllRequired ? 'All required fields provided. Ready to create campaign.' : `Missing required information. Please provide: ${missingFields.join(', ')}`, account_analysis: accountAnalysis, field_analysis: providedFields, validation_results: emailValidation, step_by_step_guidance: guidance, comprehensive_guide: fullGuidance, ready_for_next_stage: hasAllRequired && eligibleAccounts.length > 0 && (!emailValidation || emailValidation.validation_passed) }; }
- src/validation.ts:97-145 (schema)Zod schema for create_campaign prerequisite validation, used in two-step workflow for flexible initial checks.export const CreateCampaignPrerequisiteSchema = z.object({ // Workflow control stage: CampaignStageSchema.optional(), confirm_creation: z.boolean().optional(), // Optional campaign fields for prerequisite check name: z.string() .min(1, { message: 'Campaign name cannot be empty' }) .max(255, { message: 'Campaign name cannot exceed 255 characters' }) .optional(), subject: z.string() .min(1, { message: 'Subject line cannot be empty' }) .max(255, { message: 'Subject line cannot exceed 255 characters' }) .optional(), body: z.string() .min(1, { message: 'Email body cannot be empty' }) .optional(), // Message shortcut for quick setup message: z.string().optional(), email_list: z.array(EmailSchema) .min(1, { message: 'At least one email address is required' }) .max(100, { message: 'Cannot specify more than 100 email addresses' }) .optional(), // Optional scheduling parameters timezone: TimezoneSchema.optional(), timing_from: TimeFormatSchema.optional(), timing_to: TimeFormatSchema.optional(), days: DaysConfigSchema, // Optional campaign settings daily_limit: z.number().int().min(1).max(1000).optional(), email_gap_minutes: z.number().int().min(1).max(1440).optional(), // Sequence parameters sequence_steps: z.number().int().min(1).max(10).optional(), sequence_bodies: z.array(z.string()).optional(), sequence_subjects: z.array(z.string()).optional(), continue_thread: z.boolean().optional(), // Tracking settings open_tracking: z.boolean().optional(), link_tracking: z.boolean().optional(), stop_on_reply: z.boolean().optional() });