Back to Activepieces

Trigger Patterns

.agents/skills/piece-builder/trigger-patterns.md

0.87.09.7 KB
Original Source

Trigger Patterns

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.


Polling Trigger

Use when the API does NOT support webhooks. Activepieces polls every ~5 minutes.

Two deduplication strategies:

  • TIMEBASED -- Each item has a timestamp; only items newer than last poll are returned
  • LAST_ITEM -- Each item has an ID; items after the last known ID are returned

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.

TIMEBASED Polling (most common)

typescript
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

LAST_ITEM Polling

Use when items have IDs but no reliable timestamps:

typescript
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,
    }));
  },
};

Polling with Props

When the trigger has user-configurable props (e.g., a project filter), update the Polling generic type to include them:

typescript
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.


Webhook Trigger

Use when the API supports webhook registration. The flow:

  1. onEnable -- Register a webhook with the third-party API using context.webhookUrl
  2. run -- Process incoming webhook payloads
  3. onDisable -- Delete the webhook when the flow is turned off
typescript
import { 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

Webhook with Nested Event Data

Many APIs wrap the event data. Extract the relevant part:

typescript
async run(context) {
  const payload = context.payload.body as { data: { object: unknown } };
  return [payload.data.object];  // Stripe pattern
}

Webhook Handshake (Challenge-Response)

Some APIs (Slack, Okta) send a verification challenge on registration:

typescript
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
});

Webhook Renewal

For APIs where webhooks expire (e.g., Google Sheets):

typescript
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);
  },
  // ...
});

Trigger Strategy Summary

StrategyWhen to UseKey Points
TriggerStrategy.POLLINGAPI has no webhooksUse pollingHelper with TIMEBASED or LAST_ITEM
TriggerStrategy.WEBHOOKAPI supports webhook registrationRegister in onEnable, delete in onDisable
TriggerStrategy.APP_WEBHOOKOAuth2 apps with platform-level webhooks (Slack)Use context.app.createListeners()

AI-Ready Metadata (required on new triggers)

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.