> ## Documentation Index
> Fetch the complete documentation index at: https://docs.noxus.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Polling Triggers

> Start a workflow run for each new event a plugin discovers by polling an external service

A **polling trigger** starts a workflow run whenever something new happens in an external service. The platform calls your trigger on a fixed interval; you check the service, return the new events, and hand back a bit of state so the next poll knows where it left off.

Each event you return becomes the **inputs** of a triggered workflow run.

## The class

Subclass `BasePollingTrigger[Config]` and implement `poll`:

```python theme={null}
from noxus_sdk.triggers import BasePollingTrigger, TriggerConfiguration
from noxus_sdk.ncl import Parameter, ConfigText
from noxus_sdk.plugins.context import RemoteExecutionContext


class NewTicketConfig(TriggerConfiguration):
    queue: str = Parameter(
        default="",
        description="Queue to watch",
        display=ConfigText(label="Queue name"),
    )


class NewTicket(BasePollingTrigger[NewTicketConfig]):
    trigger_name = "NewTicket"          # stable id, unique within the plugin
    title = "New ticket"
    description = "Runs once per new ticket in a queue"
    integrations = ["helpdesk"]          # credentials this trigger needs
    polling_interval = 60.0              # seconds between polls
    outputs = {"ticket_id": "str"}       # event fields → type labels

    async def poll(
        self, ctx: RemoteExecutionContext, state: dict
    ) -> tuple[list[dict], dict]:
        seen: list[str] = list(state.get("seen", []))
        client = _client_from_ctx(ctx)

        tickets = await client.list_tickets(self.config.queue)
        events: list[dict] = []
        for ticket in tickets:
            ticket_id = str(ticket["id"])
            if ticket_id in seen:
                continue
            events.append({"ticket_id": ticket_id})   # keys must match `outputs`
            seen.append(ticket_id)

        return events, {"seen": seen}
```

Register it from the plugin:

```python theme={null}
def triggers(self) -> list[type[BasePollingTrigger]]:
    return [NewTicket]
```

## How `poll` works

<Steps>
  <Step title="Called on a schedule">
    The platform invokes `poll(ctx, state)` every `polling_interval` seconds. `state` is whatever your previous poll returned (an empty `dict` on the first call).
  </Step>

  <Step title="Return new events">
    Return `(events, new_state)`. Each event is a JSON-serializable `dict` whose keys **match your `outputs`** — those fields become the triggered run's inputs.
  </Step>

  <Step title="Persist a cursor">
    Put a watermark (last-seen id/timestamp, or a set of processed ids) in `new_state` so the next poll only returns genuinely new events. The platform stores it for you between polls.
  </Step>
</Steps>

<Note>
  Return an **empty list** when nothing is new — that is the normal case, and it starts no runs. Only return an event the first time you see it; deduping via `state` is what keeps a run from firing twice for the same item.
</Note>

## Reading credentials

List the integration types the trigger needs in `integrations`, then read them from the context the same way a node does:

```python theme={null}
creds = ctx.get_integration_credentials("helpdesk") or {}
api_key = creds.get("api_key", "")
```

See [Creating integrations](/developers/plugins/creating-integrations) for the credential model.

## Errors

Raise `IntegrationFailedError` (from `noxus_sdk.errors`) when the external call fails — only the message crosses the sandbox boundary, so make it actionable. Don't let an exception escape for an *expected* "nothing new" result; return an empty list instead.

```python theme={null}
from noxus_sdk.errors import IntegrationFailedError

resp = await client.list_tickets(self.config.queue)
if resp.status_code != 200:
    raise IntegrationFailedError(f"Failed to list tickets: {resp.status_code}")
```

## Definition fields

| Field                   | Required | Notes                                                                         |
| ----------------------- | -------- | ----------------------------------------------------------------------------- |
| `trigger_name`          | yes      | Stable id, unique within the plugin.                                          |
| `title` / `description` | no       | Shown in the editor; `title` defaults to `trigger_name`.                      |
| `integrations`          | no       | Credential types the trigger reads (`[]` if none).                            |
| `polling_interval`      | no       | Seconds between polls (default `300`).                                        |
| `outputs`               | no       | `{field: "type label"}` — the shape of each event / the trigger's run inputs. |
| `image`                 | no       | Icon URL.                                                                     |
