# Event listener trigger

The Event Listener trigger polls an external source on a timer and runs your workflow **once per new item**. Unlike Webhook (which waits for the source to push) or Schedule (which fires blindly), Event Listener actively pulls new data and dedupes it so you only see each item once.

Three sources are built in:

- **HTTP poll** — call any JSON GET endpoint
- **Gmail** — poll an inbox using a Gmail credential
- **Notion** — poll a database using a Notion integration

## Add it to a workflow

1. Drop an **Event Listener** node from the palette onto the canvas.
2. Pick a **Source** (HTTP poll / Gmail / Notion).
3. Fill the source-specific fields (below).
4. Set **Polling interval** (seconds, minimum **30**, default 300).
5. Pick a **Catch-up policy** — same semantics as Schedule (`skip` / `fire_once` / `fire_all`).
6. Toggle **Listener Enabled** and save.

The trigger is only registered server-side once you **save the workflow**. The **Poll now (smoke test)** button stays disabled until then.

## HTTP poll

Calls any GET (or POST) endpoint that returns JSON containing an array of items, and dedupes by an id field.

| Field          | What it does                                                                                          |
| -------------- | ----------------------------------------------------------------------------------------------------- |
| **URL**        | The endpoint to call. Must start with `http://` or `https://`.                                     |
| **Method**     | `GET` (default) or `POST`.                                                                          |
| **Headers JSON** | Extra headers as a JSON object, e.g. `{"Accept":"application/json"}`.                                |
| **Body**       | Request body (for POST). Sent verbatim.                                                                |
| **Item path**  | Dot-path from the JSON root to the items array. `items` for `{"items":[...]}`, `data.results` for nested, **empty** if the array is at the root. |
| **ID field**   | Dot-path to the unique id inside each item — `id`, `uuid`, `message_id`, etc. Items missing this field are skipped. |
| **Pagination** | None, page number, cursor, or a next URL returned by the API. |
| **Page size**  | Number of items requested per page (1–500). |
| **Max pages**  | Safety limit per polling cycle (1–50, default 10). Processing resumes next cycle. |
| **Max items**  | Maximum new workflow runs per cycle (1–5000, default 500). |
| **Credential** | Optional saved credential (`http_bearer` / `http_apikey` / `custom`) — overrides the inline token. |
| **Bearer token** | Inline fallback if no credential is selected.                                                        |

The source must return JSON (not XML/RSS/CSV), and each item must have a stable id. Page-number pagination stops on a short page or can use a boolean **Has-more path**. Cursor mode reads the next cursor from a configured response path. Next-URL mode follows relative or absolute links, with SSRF validation on every URL. If a safety limit is reached, the continuation position is stored and resumed on the next polling cycle.

## Gmail

Polls a Gmail mailbox and fires once per new message.

| Field            | What it does                                                                |
| ---------------- | --------------------------------------------------------------------------- |
| **Query**        | Gmail search query, e.g. `is:unread from:billing@`.                        |
| **Label IDs**    | Comma-separated label IDs to filter by (`INBOX`, `Label_123`).            |
| **Credential**   | Saved `gmail` credential. Required in production.                          |
| **Bearer access token** | Inline fallback for testing.                                          |

Dedupe is automatic by Gmail message id.

## Notion

Polls a Notion database and fires once per new page.

| Field             | What it does                                                                          |
| ----------------- | ------------------------------------------------------------------------------------- |
| **Database ID**   | The 32-char Notion database id (from its URL).                                        |
| **Filter JSON**   | Optional Notion query filter, e.g. `{"property":"Status","status":{"equals":"To do"}}`. |
| **Credential**    | Saved `notion` credential. Required in production.                                   |
| **Integration token** | Inline fallback for testing.                                                      |

Dedupe is automatic by Notion page id.

## What the workflow receives

The listener runs the workflow **once per new item** found. Each run sees:

```json
{
  "trigger_kind": "event_listener",
  "event": {
    "event_id": "msg_abc123",
    "payload": { ... }
  },
  "input":       { ... },
  "last_output": { ... }
}
```

`event.payload` is the raw item — a Gmail message resource, a Notion page object, or an element from your HTTP poll's `item_path`. `input` and `last_output` mirror the payload so downstream nodes can template `{last_output}` directly. Reference any field with `{event.payload.subject}`, `{event.payload.properties.Status.status.name}`, etc.

If 10 new items appear between polls, the workflow runs 10 times — one per item.

## Testing: "Poll now" button

The config panel has a **Poll now (smoke test)** button. It runs the polling adapter **once, synchronously**, against your live config and reports back:

- **URL reached — N event(s) found** with a preview of up to 10 events, or
- **Polling failed** with the exact error.

Two caveats:

1. The button only enables **after** the workflow is saved (otherwise the trigger has no server-side id).
2. Items returned by **Poll now** are added to the dedup state — the next real tick **won't re-fire them**. To replay, reset the upstream source or delete and recreate the trigger.

## Troubleshooting

- **0 events found, but items exist** — wrong **Item path** (array isn't where you think) or wrong **ID field** (every item is skipped). Check the source response shape.
- **`401` / `403` from upstream** — credential expired or wrong scope. Re-authenticate from the Credentials page.
- **Same item fires twice** — your **ID field** isn't stable. Pick a field that doesn't change between polls.
- **Listener never fires** — workflow not saved, agent not **active** (Live toggle in the Production space), or **Enabled** toggle off. Trigger health dot on the node shows the live state.
- **Items appear minutes late** — that's the polling interval. Drop it (min 30s) for more responsive fires. The dispatcher itself ticks every 15 s, so polls land within ~15 s of their due time.

---

*Source: https://agentbuilder.systalink.sn/docs/trigger-event-listener — human documentation.*
*Other language: [/docs-md/fr/trigger-event-listener.md](/docs-md/fr/trigger-event-listener.md).*
*Machine-readable index: [/llms.txt](/llms.txt).*
