agentleFS
proffesor-for-testingproffesor-for-testing/agentic-qe/.claude/skills/n8n-trigger-testing-strategies/SKILL.md

n8n-trigger-testing-strategies

Skill515 starsChanged 22 days agoMITView on GitHub

Webhook testing, schedule validation, event-driven triggers, and polling mechanism testing for n8n workflows. Use when testing how workflows are triggered.

  • Sends data out

We split this skill into the rules it gives an agent

7 rules. About 0 are common, 7 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈7

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When testing n8n triggers:

    line 24
  2. Distinctive:

    TEST with various valid payloads

    line 26
  3. Distinctive:

    VERIFY authentication and authorization

    line 27
  4. Distinctive:

    CHECK error handling for invalid inputs

    line 28
  5. Distinctive:

    Test edge cases (empty payloads, large payloads)

    line 39
  6. Distinctive:

    Verify idempotency where needed

    line 40
  7. Distinctive:

    Key patterns: Test with various payloads (valid, invalid, edge cases).

    line 546

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

n8n Trigger Testing Strategies

<default_to_action> When testing n8n triggers:

  1. IDENTIFY trigger type (webhook, schedule, polling, event)
  2. TEST with various valid payloads
  3. VERIFY authentication and authorization
  4. CHECK error handling for invalid inputs
  5. MEASURE response time and reliability

Quick Trigger Checklist:

  • Trigger activates workflow correctly
  • Payload parsed and validated
  • Authentication enforced (if configured)
  • Error responses are informative
  • Response time is acceptable

Critical Success Factors:

  • Test edge cases (empty payloads, large payloads)
  • Verify idempotency where needed
  • Check timeout handling
  • Monitor for missed triggers </default_to_action>

Quick Reference Card

n8n Trigger Types

TypeUse CaseTesting Focus
WebhookExternal HTTP callsPayloads, auth, methods
ScheduleTimed executionCron accuracy, timezone
PollingCheck for changesInterval, deduplication
EventService eventsEvent handling, filtering

Common Webhook Configurations

SettingOptionsImpact
HTTP MethodGET, POST, PUT, DELETERequest handling
AuthenticationNone, Basic, HeaderSecurity
Response ModeImmediately, Last Node, CustomResponse timing
PathCustom URL pathEndpoint identification

Webhook Testing

Basic Webhook Test

// Test webhook with various payloads
async function testWebhook(webhookUrl: string): Promise<WebhookTestResult> {
  const testPayloads = [
    // Valid JSON
    { type: 'json', data: { event: 'test', timestamp: Date.now() } },
    // Empty object
    { type: 'empty', data: {} },
    // Large payload
    { type: 'large', data: { items: Array(1000).fill({ id: 1, name: 'test' }) } },
    // Nested data
    { type: 'nested', data: { level1: { level2: { level3: { value: 'deep' } } } } },
    // Special characters
    { type: 'special', data: { text: 'Hello <script>alert("xss")</script>' } }
  ];

  const results: PayloadTestResult[] = [];

  for (const payload of testPayloads) {
    const startTime = Date.now();

    try {
      const response = await fetch(webhookUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload.data)
      });

      results.push({
        payloadType: payload.type,
        success: response.ok,
        status: response.status,
        responseTime: Date.now() - startTime,
        responseBody: await response.text()
      });
    } catch (error) {
      results.push({
        payloadType: payload.type,
        success: false,
        error: error.message
      });
    }
  }

  return { webhookUrl, results };
}

HTTP Method Testing

// Test all HTTP methods
async function testWebhookMethods(webhookUrl: string): Promise<MethodTestResult[]> {
  const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
  const results: MethodTestResult[] = [];

  for (const method of methods) {
    try {
      const response = await fetch(webhookUrl, {
        method,
        headers: { 'Content-Type': 'application/json' },
        body: ['GET', 'HEAD', 'OPTIONS'].includes(method) ? undefined : '{}'
      });

      results.push({
        method,
        allowed: response.ok || response.status !== 405,
        status: response.status,
        statusText: response.statusText
      });
    } catch (error) {
      results.push({
        method,
        allowed: false,
        error: error.message
      });
    }
  }

  return results;
}

Authentication Testing

// Test webhook authentication
async function testWebhookAuth(webhookUrl: string, authConfig: AuthConfig): Promise<AuthTestResult> {
  const scenarios = [
    // No auth
    { name: 'no-auth', headers: {} },
    // Invalid auth
    { name: 'invalid-auth', headers: { 'Authorization': 'Bearer invalid-token' } },
    // Valid auth
    { name: 'valid-auth', headers: { 'Authorization': `Bearer ${authConfig.token}` } },
    // Expired auth
    { name: 'expired-auth', headers: { 'Authorization': `Bearer ${authConfig.expiredToken}` } }
  ];

  const results: AuthScenarioResult[] = [];

  for (const scenario of scenarios) {
    const response = await fetch(webhookUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...scenario.headers
      },
      body: '{}'
    });

    results.push({
      scenario: scenario.name,
      status: response.status,
      authenticated: response.ok,
      errorMessage: response.ok ? null : await response.text()
    });
  }

  return {
    authRequired: !results.find(r => r.scenario === 'no-auth')?.authenticated,
    invalidRejected: !results.find(r => r.scenario === 'invalid-auth')?.authenticated,
    validAccepted: results.find(r => r.scenario === 'valid-auth')?.authenticated,
    results
  };
}

Schedule Testing

Cron Expression Validation

// Validate cron expression
function validateCronExpression(expression: string): CronValidationResult {
  const parts = expression.trim().split(/\s+/);

  if (parts.length < 5 || parts.length > 6) {
    return {
      valid: false,
      error: `Expected 5-6 parts, got ${parts.length}`
    };
  }

  const [minute, hour, dayOfMonth, month, dayOfWeek, year] = parts;

  const validations = [
    { field: 'minute', value: minute, range: [0, 59] },
    { field: 'hour', value: hour, range: [0, 23] },
    { field: 'dayOfMonth', value: dayOfMonth, range: [1, 31] },
    { field: 'month', value: month, range: [1, 12] },
    { field: 'dayOfWeek', value: dayOfWeek, range: [0, 7] }
  ];

  for (const v of validations) {
    const result = validateCronField(v.value, v.range);
    if (!result.valid) {
      return { valid: false, error: `Invalid ${v.field}: ${result.error}` };
    }
  }

  return {
    valid: true,
    description: describeCronExpression(expression),
    nextExecutions: getNextCronExecutions(expression, 5)
  };
}

// Get human-readable description
function describeCronExpression(expression: string): string {
  // Common patterns
  const patterns: Record<string, string> = {
    '* * * * *': 'Every minute',
    '*/5 * * * *': 'Every 5 minutes',
    '0 * * * *': 'Every hour',
    '0 0 * * *': 'Every day at midnight',
    '0 9 * * 1-5': 'Weekdays at 9:00 AM',
    '0 0 1 * *': 'First day of every month',
    '0 0 * * 0': 'Every Sunday at midnight'
  };

  return patterns[expression] || 'Custom schedule';
}

// Calculate next execution times
function getNextCronExecutions(expression: string, count: number): Date[] {
  const executions: Date[] = [];
  let current = new Date();

  // Simple implementation - use cron-parser library in production
  for (let i = 0; i < count; i++) {
    const next = calculateNextCronExecution(expression, current);
    executions.push(next);
    current = new Date(next.getTime() + 60000); // Move past this execution
  }

  return executions;
}

Schedule Reliability Testing

// Test schedule trigger reliability
async function testScheduleReliability(triggerId: string, testDuration: number): Promise<ScheduleTestResult> {
  const startTime = Date.now();
  const expectedExecutions: Date[] = [];
  const actualExecutions: Date[] = [];

  // Calculate expected execution times
  const cronExpression = await getTriggerCronExpression(triggerId);
  let checkTime = new Date(startTime);
  while (checkTime.getTime() < startTime + testDuration) {
    const nextExec = calculateNextCronExecution(cronExpression, checkTime);
    if (nextExec.getTime() < startTime + testDuration) {
      expectedExecutions.push(nextExec);
    }
    checkTime = new Date(nextExec.getTime() + 60000);
  }

  // Monitor actual executions
  const executionListener = onExecutionStart(triggerId, (exec) => {
    actualExecutions.push(new Date(exec.startedAt));
  });

  // Wait for test duration
  await sleep(testDuration);
  executionListener.stop();

  // Compare expected vs actual
  const comparison = compareExecutions(expectedExecutions, actualExecutions);

  return {
    testDuration,
    expectedCount: expectedExecutions.length,
    actualCount: actualExecutions.length,
    missedExecutions: comparison.missed,
    extraExecutions: comparison.extra,
    timingAccuracy: comparison.timingAccuracy,
    reliability: (actualExecutions.length / expectedExecutions.length) * 100
  };
}

Polling Trigger Testing

// Test polling trigger behavior
async function testPollingTrigger(triggerId: string, testConfig: PollingTestConfig): Promise<PollingTestResult> {
  const { interval, testDuration, simulateDataChanges } = testConfig;

  const pollEvents: PollEvent[] = [];
  const triggeredExecutions: Execution[] = [];

  // Monitor polling events
  const pollListener = onPoll(triggerId, (event) => {
    pollEvents.push({
      timestamp: new Date(),
      dataFound: event.hasNewData,
      itemCount: event.items?.length || 0
    });
  });

  // Monitor triggered executions
  const execListener = onExecutionStart(triggerId, (exec) => {
    triggeredExecutions.push(exec);
  });

  // Optionally simulate data changes
  if (simulateDataChanges) {
    for (const change of simulateDataChanges) {
      setTimeout(() => {
        injectTestData(triggerId, change.data);
      }, change.at);
    }
  }

  // Wait for test duration
  await sleep(testDuration);

  pollListener.stop();
  execListener.stop();

  // Analyze results
  const expectedPolls = Math.floor(testDuration / interval);
  const actualPolls = pollEvents.length;

  return {
    interval,
    testDuration,
    expectedPolls,
    actualPolls,
    pollAccuracy: (actualPolls / expectedPolls) * 100,
    averageInterval: calculateAverageInterval(pollEvents),
    executionsTriggered: triggeredExecutions.length,
    deduplicationWorking: checkDeduplication(triggeredExecutions),
    pollEvents
  };
}

// Check if deduplication is working
function checkDeduplication(executions: Execution[]): boolean {
  const processedIds = new Set();

  for (const exec of executions) {
    const itemIds = exec.data?.resultData?.runData?.Trigger?.[0]?.data?.main?.[0]
      ?.map(item => item.json?.id);

    if (itemIds) {
      for (const id of itemIds) {
        if (processedIds.has(id)) {
          return false; // Duplicate found
        }
        processedIds.add(id);
      }
    }
  }

  return true;
}

Event Trigger Testing

// Test event-driven triggers
async function testEventTrigger(triggerId: string, eventConfig: EventTestConfig): Promise<EventTestResult> {
  const { eventType, testEvents, timeout } = eventConfig;

  const results: EventResult[] = [];

  for (const testEvent of testEvents) {
    // Emit test event
    const startTime = Date.now();
    await emitTestEvent(eventType, testEvent.payload);

    // Wait for trigger
    try {
      const execution = await waitForTrigger(triggerId, timeout);

      results.push({
        eventType: testEvent.type,
        triggered: true,
        latency: Date.now() - startTime,
        payloadReceived: execution.data?.inputData
      });
    } catch (error) {
      results.push({
        eventType: testEvent.type,
        triggered: false,
        error: error.message
      });
    }
  }

  return {
    eventType,
    testsRun: testEvents.length,
    triggered: results.filter(r => r.triggered).length,
    averageLatency: average(results.filter(r => r.triggered).map(r => r.latency)),
    results
  };
}

Trigger Response Testing

// Test trigger response modes
async function testTriggerResponses(webhookUrl: string): Promise<ResponseTestResult> {
  // Test immediate response
  const immediateStart = Date.now();
  const immediateResponse = await fetch(webhookUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: '{"test": "immediate"}'
  });
  const immediateTime = Date.now() - immediateStart;

  // Test with workflow execution
  const workflowStart = Date.now();
  const workflowResponse = await fetch(`${webhookUrl}?waitForResponse=true`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: '{"test": "workflow"}'
  });
  const workflowTime = Date.now() - workflowStart;

  return {
    immediateResponse: {
      status: immediateResponse.status,
      time: immediateTime,
      body: await immediateResponse.text()
    },
    workflowResponse: {
      status: workflowResponse.status,
      time: workflowTime,
      body: await workflowResponse.text()
    },
    responseMode: workflowTime > immediateTime + 100 ? 'workflow' : 'immediate'
  };
}

Test Scenarios

Webhook Scenarios:
  - name: Valid JSON POST
    method: POST
    payload: {"event": "test"}
    expected: 200 OK

  - name: Invalid JSON
    method: POST
    payload: "not valid json"
    expected: 400 Bad Request

  - name: Missing auth
    method: POST
    headers: {}
    expected: 401 Unauthorized

  - name: Large payload
    method: POST
    payload: [10MB of data]
    expected: 413 Payload Too Large

Schedule Scenarios:
  - name: Every 5 minutes
    cron: "*/5 * * * *"
    verify: 12 executions per hour

  - name: Weekdays at 9 AM
    cron: "0 9 * * 1-5"
    verify: 5 executions per week

Polling Scenarios:
  - name: 1 minute interval
    interval: 60000
    verify: ~60 polls per hour

  - name: Deduplication
    interval: 60000
    inject_duplicate: true
    verify: No duplicate processing

Related Skills

  • n8n-workflow-testing-fundamentals
  • n8n-integration-testing-patterns
  • n8n-security-testing

Remember

n8n triggers are the entry points to workflows. Testing requires:

  • Webhook: Payload handling, auth, HTTP methods
  • Schedule: Cron accuracy, timezone handling
  • Polling: Interval accuracy, deduplication
  • Event: Event handling, filtering

Key patterns: Test with various payloads (valid, invalid, edge cases). Verify authentication enforcement. Check response times and reliability over time.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.