.agents/skills/piece-builder/trigger-patterns.md
Two main types: Polling (check API periodically) and Webhook (receive push notifications).
Prefer webhooks when the API supports them -- they are instant and use fewer resources.
Use when the API does NOT support webhooks. Activepieces polls every ~5 minutes.
Two deduplication strategies:
Always pass the whole context to pollingHelper.onEnable / onDisable / poll / test -- never a subset like { store, auth, propsValue }. Every field on the helper's param type is optional, so a partial object type-checks and whatever you left out is dropped in silence. The set also grows: onEnable now reads context.isRepublish to keep the existing lastPoll/lastItem when a running flow is republished, so a subset call still resets the checkpoint and drops every event since the last poll.
Editing an existing polling trigger? Fix its call while you're there. Most pieces in the repo still pass the subset. If you touch one -- new trigger, bug fix, anything -- switch every pollingHelper call in that piece to pass context. You are already bumping the version and rebuilding, so the fix costs nothing extra and gets verified with your change; a one-shot codemod across every piece instead means a huge unreviewable diff and a forced version bump on pieces nobody runs. Mention the fix in your PR description so it doesn't read as an unrelated change.
import { createTrigger, TriggerStrategy, AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
import { DedupeStrategy, Polling, pollingHelper, httpClient, HttpMethod, AuthenticationType } from '@activepieces/pieces-common';
import { myAppAuth } from '../../';
const polling: Polling<AppConnectionValueForAuthProperty<typeof myAppAuth>, Record<string, never>> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
const response = await httpClient.sendRequest<{ data: any[] }>({
method: HttpMethod.GET,
url: 'https://api.example.com/v1/records',
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth.secret_text,
},
queryParams: {
sort: 'created_at',
order: 'desc',
limit: '100',
},
});
return response.body.data.map((item) => ({
epochMilliSeconds: new Date(item.created_at).getTime(),
data: item,
}));
},
};
export const newRecordTrigger = createTrigger({
auth: myAppAuth,
name: 'new_record',
displayName: 'New Record',
description: 'Triggers when a new record is created',
props: {},
sampleData: {},
type: TriggerStrategy.POLLING,
async test(context) {
return await pollingHelper.test(polling, context);
},
async onEnable(context) {
await pollingHelper.onEnable(polling, context);
},
async onDisable(context) {
await pollingHelper.onDisable(polling, context);
},
async run(context) {
return await pollingHelper.poll(polling, context);
},
});
Real example: packages/pieces/community/airtable/src/lib/trigger/new-record.trigger.ts
Use when items have IDs but no reliable timestamps:
const polling: Polling<undefined, Record<string, never>> = {
strategy: DedupeStrategy.LAST_ITEM,
items: async ({ auth, propsValue, lastItemId }) => {
const response = await httpClient.sendRequest<any[]>({
method: HttpMethod.GET,
url: 'https://api.example.com/v1/records',
queryParams: { sort: 'id', order: 'desc', limit: '50' },
});
return response.body.map((item) => ({
id: item.id, // Unique identifier
data: item,
}));
},
};
When the trigger has user-configurable props (e.g., a project filter), update the Polling generic type to include them:
const props = { projectId: Property.Dropdown({ /* ... */ }) };
const polling: Polling<
AppConnectionValueForAuthProperty<typeof myAppAuth>,
StaticPropsValue<typeof props> // ← add your props type here
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue }) => {
// propsValue.projectId is now available and typed
const response = await httpClient.sendRequest<{ data: any[] }>({
method: HttpMethod.GET,
url: `https://api.example.com/v1/projects/${propsValue.projectId}/records`,
// ...
});
return response.body.data.map((item) => ({
epochMilliSeconds: new Date(item.created_at).getTime(),
data: item,
}));
},
};
Pass props to createTrigger — everything else follows the same pattern as the basic TIMEBASED example above.
Use when the API supports webhook registration. The flow:
onEnable -- Register a webhook with the third-party API using context.webhookUrlrun -- Process incoming webhook payloadsonDisable -- Delete the webhook when the flow is turned offimport { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod, AuthenticationType } from '@activepieces/pieces-common';
import { myAppAuth } from '../../';
export const newRecordWebhookTrigger = createTrigger({
auth: myAppAuth,
name: 'new_record_webhook',
displayName: 'New Record',
description: 'Triggers when a new record is created',
props: {},
sampleData: {
id: '123',
name: 'Example record',
created_at: '2024-01-01T00:00:00Z',
},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
// Register webhook with the external service
const response = await httpClient.sendRequest<{ id: string }>({
method: HttpMethod.POST,
url: 'https://api.example.com/v1/webhooks',
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.secret_text,
},
body: {
url: context.webhookUrl, // Activepieces provides this
events: ['record.created'],
},
});
// Store webhook ID for cleanup
await context.store.put('webhookId', response.body.id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>('webhookId');
if (webhookId) {
await httpClient.sendRequest({
method: HttpMethod.DELETE,
url: `https://api.example.com/v1/webhooks/${webhookId}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.secret_text,
},
});
}
},
async run(context) {
// Return webhook payload as array (each element becomes a separate flow run)
return [context.payload.body];
},
async test(context) {
// Optional: fetch recent items for testing in the UI
const response = await httpClient.sendRequest<{ data: any[] }>({
method: HttpMethod.GET,
url: 'https://api.example.com/v1/records',
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.secret_text,
},
queryParams: { limit: '5' },
});
return response.body.data || [];
},
});
Real example: packages/pieces/community/stripe/src/lib/trigger/new-customer.ts
Many APIs wrap the event data. Extract the relevant part:
async run(context) {
const payload = context.payload.body as { data: { object: unknown } };
return [payload.data.object]; // Stripe pattern
}
Some APIs (Slack, Okta) send a verification challenge on registration:
import { WebhookHandshakeStrategy } from '@activepieces/pieces-framework';
export const myTrigger = createTrigger({
// ...
type: TriggerStrategy.WEBHOOK,
handshakeConfiguration: {
strategy: WebhookHandshakeStrategy.HEADER_PRESENT,
paramName: 'x-verification-challenge',
},
async onHandshake(context) {
const challenge = context.payload.headers['x-verification-challenge'];
return {
status: 200,
body: { challenge },
headers: { 'Content-Type': 'application/json' },
};
},
// ... rest of trigger
});
For APIs where webhooks expire (e.g., Google Sheets):
import { WebhookRenewStrategy } from '@activepieces/pieces-framework';
export const myTrigger = createTrigger({
// ...
renewConfiguration: {
strategy: WebhookRenewStrategy.CRON,
cronExpression: '0 */12 * * *', // Every 12 hours
},
async onRenew(context) {
// Delete old webhook and create new one
const oldId = await context.store.get<string>('webhookId');
if (oldId) await deleteWebhook(oldId, context.auth);
const newWebhook = await createWebhook(context.webhookUrl, context.auth);
await context.store.put('webhookId', newWebhook.id);
},
// ...
});
| Strategy | When to Use | Key Points |
|---|---|---|
TriggerStrategy.POLLING | API has no webhooks | Use pollingHelper with TIMEBASED or LAST_ITEM |
TriggerStrategy.WEBHOOK | API supports webhook registration | Register in onEnable, delete in onDisable |
TriggerStrategy.APP_WEBHOOK | OAuth2 apps with platform-level webhooks (Slack) | Use context.app.createListeners() |
Every new trigger ships with aiMetadata: { description } — one or two sentences on when the event fires and what one payload represents. Triggers do not take audience (actions-only — a trigger is an event, not an agent-callable operation) and don't need idempotent. The field is additive and changes nothing for human users. See ai-metadata.md.