Install
$ agentstack add skill-impertio-studio-n8n-claude-skill-package-n8n-syntax-trigger-nodes ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
n8n Trigger Node Development
> Build custom trigger nodes for n8n v1.x using event/timer, polling, or webhook patterns.
Quick Reference
| Pattern | Method | Interface | Description Key | Use When | |---------|--------|-----------|-----------------|----------| | Event/Timer | trigger() | ITriggerFunctions | group: ['trigger'] | Listening to events, cron schedules, streams | | Polling | poll() | IPollFunctions | polling: true | Periodically checking an API for new data | | Webhook | webhook() | IWebhookFunctions | webhooks: [...] | External service sends HTTP requests to n8n |
Critical Rules
- ALWAYS set
inputs: []on trigger nodes — triggers have NO inputs - ALWAYS include
'trigger'in thegrouparray - ALWAYS double-wrap emitted data:
this.emit([[items]])— outer array = outputs, inner array = items - ALWAYS provide
manualTriggerFunctionfor event triggers so "Test Workflow" works in the editor - ALWAYS implement
closeFunctionwhen your trigger allocates resources (intervals, connections, listeners) - NEVER emit single-wrapped arrays —
this.emit([items])causes silent failures - NEVER forget cleanup — leaked intervals/connections persist across workflow deactivation
Decision Tree: Which Trigger Pattern?
Need a trigger node?
├── Does an EXTERNAL SERVICE call YOUR endpoint?
│ └── YES → Use WEBHOOK pattern (webhook() + IWebhookFunctions)
│ ├── Service has webhook registration API? → Add webhookMethods lifecycle
│ └── Service sends to a static URL? → Use webhooks[] description only
├── Do you need to CHECK an API periodically?
│ └── YES → Use POLLING pattern (poll() + IPollFunctions)
│ └── ALWAYS implement deduplication (track last seen ID/timestamp)
└── Do you LISTEN for events, run on schedule, or stream data?
└── YES → Use EVENT/TIMER pattern (trigger() + ITriggerFunctions)
└── ALWAYS implement closeFunction for cleanup
Pattern 1: Event/Timer Trigger
Use trigger() with ITriggerFunctions for schedule-based or event-driven triggers.
Minimal Structure
import type {
ITriggerFunctions,
INodeType,
INodeTypeDescription,
ITriggerResponse,
INodeExecutionData,
} from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
export class MyEventTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'My Event Trigger',
name: 'myEventTrigger',
group: ['trigger'], // REQUIRED for trigger nodes
version: 1,
inputs: [], // ALWAYS empty for triggers
outputs: [NodeConnectionTypes.Main],
defaults: { name: 'My Event Trigger' },
properties: [/* ... */],
};
async trigger(this: ITriggerFunctions): Promise {
const executeTrigger = () => {
// CRITICAL: Double-wrap — [[items]]
this.emit([[ { json: { timestamp: new Date().toISOString() } } ]]);
};
if (this.getMode() === 'manual') {
// Editor "Test Workflow" mode
return { manualTriggerFunction: async () => executeTrigger() };
}
// Production mode — set up persistent listener
const intervalId = setInterval(executeTrigger, 60_000);
// ALWAYS return closeFunction for cleanup
return { closeFunction: async () => clearInterval(intervalId) };
}
}
ITriggerResponse Fields
| Field | Type | Purpose | |-------|------|---------| | closeFunction | () => Promise | Cleanup when workflow deactivated. ALWAYS implement when allocating resources. | | manualTriggerFunction | () => Promise | Simulates trigger for "Test Workflow" in editor. ALWAYS implement for event triggers. | | manualTriggerResponse | Promise | Alternative: promise that resolves when data is emitted. |
Emitting Data
// CORRECT — double array wrapping
this.emit([[ { json: { key: 'value' } } ]]);
// CORRECT — multiple items in one emission
this.emit([[ { json: { id: 1 } }, { json: { id: 2 } } ]]);
// CORRECT — using helper for array conversion
this.emit([this.helpers.returnJsonArray([{ id: 1 }, { id: 2 }])]);
// WRONG — single array wrapping (SILENT FAILURE)
this.emit([{ json: { key: 'value' } }]); // DO NOT DO THIS
Error Reporting
// Non-fatal: log error, workflow stays active
this.saveFailedExecution(error);
// Fatal: deactivates workflow, queues for reactivation with backoff
this.emitError(error);
Pattern 2: Polling Trigger
Use poll() with IPollFunctions for periodic API checks.
Minimal Structure
export class MyPollingTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'My Polling Trigger',
name: 'myPollingTrigger',
group: ['trigger'],
version: 1,
polling: true, // REQUIRED — marks as polling trigger
inputs: [],
outputs: [NodeConnectionTypes.Main],
defaults: { name: 'My Polling Trigger' },
properties: [
{
displayName: 'Poll Interval',
name: 'pollInterval',
type: 'options',
default: 'every5Minutes',
options: [
{ name: 'Every Minute', value: 'everyMinute' },
{ name: 'Every 5 Minutes', value: 'every5Minutes' },
],
},
],
};
async poll(this: IPollFunctions): Promise {
// Return null when no new data — NEVER return empty arrays
// Return [[items]] when new data found
}
}
Deduplication Pattern
ALWAYS implement deduplication for polling triggers to prevent duplicate processing.
async poll(this: IPollFunctions): Promise {
const webhookData = this.getWorkflowStaticData('node');
const lastTimestamp = webhookData.lastTimestamp as string | undefined;
const items = await fetchNewItems(lastTimestamp);
if (items.length === 0) {
return null; // ALWAYS return null for "no new data"
}
// Update stored state for next poll cycle
webhookData.lastTimestamp = items[items.length - 1].updatedAt;
return [items.map(item => ({ json: item }))];
}
> Key: Use this.getWorkflowStaticData('node') to persist state between poll cycles. This data survives workflow restarts. ALWAYS update the stored marker after processing.
Pattern 3: Webhook Trigger
Use webhook() with IWebhookFunctions for HTTP-triggered workflows.
Minimal Structure
export class MyWebhookTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'My Webhook Trigger',
name: 'myWebhookTrigger',
group: ['trigger'],
version: 1,
inputs: [],
outputs: [NodeConnectionTypes.Main],
defaults: { name: 'My Webhook Trigger' },
webhooks: [
{
name: 'default',
httpMethod: '={{$parameter["httpMethod"] || "POST"}}',
path: '={{$parameter["path"]}}',
responseMode: '={{$parameter["responseMode"]}}',
},
],
properties: [/* httpMethod, path, responseMode */],
};
async webhook(this: IWebhookFunctions): Promise {
const body = this.getBodyData();
return { workflowData: [[ { json: body } ]] };
}
}
WebhookResponseMode
| Mode | Behavior | Use When | |------|----------|----------| | onReceived | Respond immediately after webhook node executes | Fire-and-forget; caller does not need workflow result | | lastNode | Respond after the LAST node in workflow finishes | Caller needs the final processed result | | responseNode | Respond from a dedicated "Respond to Webhook" node | Need custom response body/status at a specific point |
IWebhookFunctions Methods
| Method | Returns | Purpose | |--------|---------|---------| | getBodyData() | IDataObject | Parsed request body | | getHeaderData() | IncomingHttpHeaders | Request headers | | getQueryData() | object | URL query parameters | | getParamsData() | object | URL path parameters | | getRequestObject() | express.Request | Full Express request (advanced) | | getResponseObject() | express.Response | Full Express response (advanced, for manual response) | | getNodeWebhookUrl('default') | string | The registered webhook URL |
Webhook Lifecycle (webhookMethods)
For triggers that register webhooks on EXTERNAL services (e.g., GitHub, Stripe):
webhookMethods: {
default: {
async checkExists(this: IHookFunctions): Promise {
// Return true if webhook already registered on external service
},
async create(this: IHookFunctions): Promise {
// Register webhook URL on external service
// Store webhook ID in static data for later deletion
const webhookUrl = this.getNodeWebhookUrl('default');
// ... API call to register ...
return true;
},
async delete(this: IHookFunctions): Promise {
// Remove webhook from external service on deactivation
return true;
},
},
},
Lifecycle: checkExists runs first. If false, create runs. On workflow deactivation, delete runs.
IWebhookResponseData Fields
| Field | Type | Purpose | |-------|------|---------| | workflowData | INodeExecutionData[][] | Data passed into the workflow | | webhookResponse | any | Custom response sent back to the caller | | noWebhookResponse | boolean | Set true if you already sent a response via getResponseObject() |
Trigger Node Description Requirements
Every trigger node description MUST include:
description: INodeTypeDescription = {
group: ['trigger'], // REQUIRED — identifies as trigger
inputs: [], // REQUIRED — triggers have no inputs
outputs: [NodeConnectionTypes.Main],
// For polling triggers, add:
polling: true,
// For webhook triggers, add:
webhooks: [{ name: 'default', httpMethod: '...', path: '...', responseMode: '...' }],
// Optional but recommended:
eventTriggerDescription: 'Waiting for events from MyService',
activationMessage: 'Your trigger is now active and listening.',
};
Reference Files
| File | Contents | |------|----------| | [references/methods.md](references/methods.md) | Complete ITriggerFunctions, IWebhookFunctions, IPollFunctions, ITriggerResponse, webhookMethods, WebhookResponseMode | | [references/examples.md](references/examples.md) | Full implementations: schedule trigger, webhook trigger, polling trigger | | [references/anti-patterns.md](references/anti-patterns.md) | Common trigger implementation mistakes and fixes |
Sources
- n8n-io/n8n GitHub:
packages/workflow/src/interfaces.ts— ITriggerFunctions, IWebhookFunctions, IPollFunctions, ITriggerResponse - n8n-io/n8n GitHub:
packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.ts - n8n-io/n8n GitHub:
packages/nodes-base/nodes/Webhook/Webhook.node.ts - n8n official docs: https://docs.n8n.io/integrations/creating-nodes/build/programmatic-style/
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Impertio-Studio
- Source: Impertio-Studio/n8n-Claude-Skill-Package
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.