# Custom Poller Example

This guide shows how to create a custom poller in your own repository that invokes fullsend [harness](../../glossary.md#harness) agents directly, bypassing the standard GitHub event trigger flow.

## Use Case

Custom pollers are useful when you want to:
- Poll external systems (Jira, Linear, Slack, etc.) on a schedule
- Trigger [harness](../../glossary.md#harness) agents based on custom logic
- Reuse fullsend's [harness](../../glossary.md#harness) infrastructure without duplicating workflow code

## Example: Jira Polling Workflow

This example polls Jira for bugs and dispatches fullsend agents to process them:

```yaml
name: Fullsend Jira Poll

on:
  schedule:
    - cron: '*/30 * * * *'  # every 30 minutes
  workflow_dispatch:

permissions:
  actions: write
  contents: write
  id-token: write
  issues: write
  packages: read
  pull-requests: write

jobs:
  poll:
    runs-on: ubuntu-24.04
    outputs:
      matrix: ${{ steps.dispatch.outputs.matrix }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install fullsend CLI
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh release download --repo fullsend-ai/fullsend \
            -p 'fullsend_*_linux_amd64.tar.gz' -O - | tar xz
          sudo mv fullsend /usr/local/bin/

      - name: Poll Jira and build dispatch matrix
        id: dispatch
        env:
          JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }}
          JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
          JIRA_BASE_URL: ${{ vars.JIRA_BASE_URL }}
          TARGET_REPO: ${{ github.repository }}
        run: |
          fullsend poll \
            --input-driver jira-poll \
            --jira-url "${JIRA_BASE_URL}" \
            --jira-project MYPROJECT \
            --jql 'project=MYPROJECT and statusCategory != Done and updated > -1week and type=Bug' \
            --target-repo "${TARGET_REPO}" \
            --output dispatches.json \
            --fullsend-dir .fullsend

          # Build GitHub Actions matrix format
          if ! jq -e 'length > 0' dispatches.json > /dev/null 2>&1; then
            echo 'matrix={"include":[]}' >> "${GITHUB_OUTPUT}"
            exit 0
          fi
          MATRIX=$(jq -c '{include: .}' dispatches.json)
          DELIM="MATRIX_$(openssl rand -hex 8)"
          {
            echo "matrix<<${DELIM}"
            printf '%s' "${MATRIX}"
            echo
            echo "${DELIM}"
          } >> "${GITHUB_OUTPUT}"

  harness:
    needs: poll
    uses: fullsend-ai/fullsend/.github/workflows/reusable-dispatch.yml@main
    with:
      matrix: ${{ needs.poll.outputs.matrix }}
      mint_url: ${{ vars.FULLSEND_MINT_URL }}
      gcp_region: ${{ vars.FULLSEND_GCP_REGION }}
      jira_base_url: ${{ vars.JIRA_BASE_URL }}
    secrets:
      FULLSEND_GCP_WIF_PROVIDER: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }}
      FULLSEND_GCP_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }}
      OTEL_EXPORTER_OTLP_TRACES_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_TRACES_HEADERS }}
      OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
      JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }}
      JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
```

## Matrix Format

The `matrix` input (a GitHub Actions [matrix strategy](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow#using-a-matrix-strategy) that runs parallel jobs) must follow the format produced by `fullsend dispatch --output-driver gha-matrix`:

```json
{
  "include": [
    {
      "agent": "agent-name",
      "source_repo": "org/repo",
      "role": "harness",
      "event_payload": "{...}",
      "status_repo": "org/repo",
      "status_number": "123"
    }
  ]
}
```

## Prerequisites

Your external repository needs these variables and secrets configured:

**Variables:**
- `FULLSEND_MINT_URL` - Token mint service URL
- `FULLSEND_GCP_REGION` - GCP region for Vertex AI
- `OTEL_EXPORTER_OTLP_ENDPOINT` - [OpenTelemetry](../../glossary.md#otel-primary-facts) endpoint (optional)
- `JIRA_BASE_URL` - Jira instance URL (if using Jira agents)

**Secrets:**
- `FULLSEND_GCP_WIF_PROVIDER` - GCP Workload Identity Federation (WIF) provider
- `FULLSEND_GCP_PROJECT_ID` - GCP project ID
- `OTEL_EXPORTER_OTLP_TRACES_HEADERS` - [OTEL](../../glossary.md#otel-primary-facts) auth headers (optional)
- `JIRA_TOKEN` - Jira API token (if using Jira agents)
- `JIRA_USER_EMAIL` - Jira user email (if using Jira agents)

## How It Works

1. **Poll job** runs your custom logic to query an external system and builds a dispatch matrix
2. **Harness job** calls `reusable-dispatch.yml` with the pre-computed matrix
3. `reusable-dispatch.yml` skips the routing and dispatch steps (see [architecture.md](../../architecture.md) for the standard dispatch flow), directly invoking `harness-run` with your matrix
4. [Harness](../../glossary.md#harness) agents execute according to your matrix configuration

## Permissions

The top-level `permissions:` block grants the maximum permissions required by `reusable-dispatch.yml`. Even though you're only running harness agents via the pre-computed matrix, GitHub validates that the caller grants sufficient permissions for all jobs in the reusable workflow (including code and fix jobs that won't actually run).

Required permissions:
- `contents: write` - needed by code/fix agents
- `packages: read` - needed by code/fix agents
- `actions: write`, `id-token: write`, `issues: write`, `pull-requests: write` - needed by all agents

`id-token: write` lets GitHub Actions issue the job a short-lived [OpenID
Connect (OIDC) token](how-fullsend-authenticates.md). Fullsend uses this token
to identify the calling workflow during dispatch and to mint short-lived
[GitHub App installation tokens](../../glossary.md#installation-token) for
agents.
Grant it in the caller workflow or on the job that calls
`reusable-dispatch.yml`; the reusable workflow cannot add permissions that the
caller did not grant. Keep the OIDC token inside the job. Do not copy it into
workflow outputs, the dispatch matrix, or logs.

## Authorization

When using a pre-computed matrix, the `.fullsend/config.yaml` agent-enablement checks are bypassed. The token mint service acts as the authorization boundary (see [mint-administration.md](../infrastructure/mint-administration.md)) - ensure your mint service is properly configured to control which agents external callers can invoke.
