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

# Built-in proactive triggers

> Complete reference for every trigger shipped in the SDK catalog — when it fires, what it detects, how to tune it, and how to enable it from JSON config.

The SDK ships three built-in triggers you can enable without writing any detection code. Each one is registered in the **built-in catalog** and selectable via `integration_config.proactive_triggers.builtins` in your `products.json`.

> Need to write your own trigger? See [Authoring proactive triggers](/sdk/proactive-triggers-authoring).

***

## How to enable built-ins

Add a `builtins` array to `integration_config.proactive_triggers`. Each row must include `id`, `name`, and `description` — these are required non-empty strings used in logs and admin UIs.

```json theme={null}
{
  "integration_config": {
    "proactive_triggers": {
      "builtins": [
        {
          "id": "canonical_url_ping_pong",
          "name": "URL hesitation",
          "description": "Fires when the user bounces between the same URLs"
        },
        {
          "id": "user_page_dwell",
          "name": "Page dwell",
          "description": "Fires when the user lingers on a page with few actions"
        }
      ]
    }
  }
}
```

**Order matters** — `evaluate_first` returns the first matching trigger. Put higher-priority triggers earlier in the list.

Two optional per-row overrides apply to any built-in:

| Field                   | Default         | Meaning                                                   |
| ----------------------- | --------------- | --------------------------------------------------------- |
| `interaction_timeout_s` | catalog default | How long the proactive UI stays open waiting for the user |
| `cooldown_s`            | catalog default | Minimum gap before this trigger can fire again            |

Invalid rows (unknown `id`, missing required fields) raise `SdkConfigError` and log `event=proactive_builtin_spec_invalid`.

***

## Built-in catalog

<AccordionGroup>
  <Accordion title="canonical_url_ping_pong — URL hesitation">
    ### What it detects

    The user is bouncing back and forth between the same canonical URLs — a strong signal of hesitation, confusion, or indecision. The trigger fires after the URL sequence shows at least `min_cycles` complete back-and-forth cycles between the same pages.

    **Example pattern that fires (min\_cycles=1):**

    ```
    /projects → /settings → /projects → /settings
    ```

    ### Stable ID

    ```python theme={null}
    from autoplay_sdk.proactive_triggers.defaults import TRIGGER_ID_CANONICAL_URL_PING_PONG
    # TRIGGER_ID_CANONICAL_URL_PING_PONG = "canonical_url_ping_pong"
    ```

    ### Default registry

    This is the only trigger included in `default_proactive_trigger_registry()`. If you omit `builtins` entirely from your config, ping-pong is active by default.

    ### Tuning

    | Parameter    | Default | Meaning                                                                                                                                           |
    | ------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `min_cycles` | `1`     | Number of complete back-and-forth URL repeats required before the trigger fires. Raise this to require stronger hesitation signals before firing. |

    `min_cycles` is set at catalog instantiation time via the JSON row — it is not a runtime `context_extra` key.

    ### Result

    | Field                 | Value                                                              |
    | --------------------- | ------------------------------------------------------------------ |
    | `trigger_id`          | `"canonical_url_ping_pong"`                                        |
    | `body`                | Default proactive copy (from `DEFAULT_PROACTIVE_QUICK_REPLY_BODY`) |
    | `metadata.min_cycles` | The `min_cycles` value used for this evaluation                    |

    ### JSON config example

    ```json theme={null}
    {
      "id": "canonical_url_ping_pong",
      "name": "URL hesitation",
      "description": "Fires when the user bounces between the same URLs",
      "interaction_timeout_s": 15,
      "cooldown_s": 60
    }
    ```

    ### Scoping to a URL prefix

    To limit ping-pong detection to a specific area of your product (e.g. only the `/projects` section), use `ScopedCanonicalPingPongTrigger` in code — this is not configurable from JSON.

    ```python theme={null}
    from autoplay_sdk.proactive_triggers.triggers.scoped_canonical_ping_pong import ScopedCanonicalPingPongTrigger

    trigger = ScopedCanonicalPingPongTrigger(url_substring="/projects")
    ```
  </Accordion>

  <Accordion title="user_page_dwell — Page linger detection">
    ### What it detects

    The user has been on the same canonical URL for longer than `dwell_threshold_seconds` **and** has performed fewer than `user_page_dwell_max_actions` actions during that time. This distinguishes passive lingering (stuck, confused, reading carefully) from active exploration (clicking around, filling forms).

    The trigger only looks at the **trailing run** of `recent_actions` that share the same `canonical_url` as the latest action — it ignores earlier visits to the same page.

    **Fires when:**

    1. The trailing URL streak spans ≥ `dwell_threshold_seconds`
    2. The streak contains ≤ `user_page_dwell_max_actions` actions

    If the user has more actions on the streak than the max, the trigger does **not** fire — the system treats it as intentional exploration, not passive linger.

    ### Stable ID

    ```python theme={null}
    from autoplay_sdk.proactive_triggers.defaults import TRIGGER_ID_USER_PAGE_DWELL
    # TRIGGER_ID_USER_PAGE_DWELL = "user_page_dwell"
    ```

    ### Tuning

    All tuning keys can be set in `integration_config.proactive_triggers` (stock connector copies them into `context_extra` automatically) or passed directly in `context_extra` when building `ProactiveTriggerContext` in code.

    | Key                           | Default                    | Meaning                                                                                                                                               |
    | ----------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `dwell_threshold_seconds`     | `60`                       | Minimum seconds on the same canonical URL streak before the trigger can fire. Must be > 0.                                                            |
    | `user_page_dwell_max_actions` | `5`                        | Maximum number of actions allowed on the trailing URL streak. More actions than this → trigger does not fire. Invalid or ≤ 0 values fall back to `5`. |
    | `dwell_proactive_body`        | *(built-in short message)* | Optional override for the proactive message body. Falls back to `"Still on this page — want a quick tip?"` if empty or omitted.                       |

    **Test-only key:** `eval_now` (Unix timestamp as `float`) — fixes the definition of "now" during unit tests so dwell duration is deterministic.

    ### Result

    | Field                                    | Value                                                                          |
    | ---------------------------------------- | ------------------------------------------------------------------------------ |
    | `trigger_id`                             | `"user_page_dwell"`                                                            |
    | `body`                                   | `dwell_proactive_body` override, or `"Still on this page — want a quick tip?"` |
    | `metadata.user_page_dwell_seconds`       | Measured dwell duration of the trailing URL streak (rounded to 3 dp)           |
    | `metadata.user_page_dwell_action_count`  | Number of actions in the trailing streak                                       |
    | `metadata.user_page_dwell_max_actions`   | The effective `user_page_dwell_max_actions` used for this evaluation           |
    | `metadata.dwell_threshold_seconds`       | The effective `dwell_threshold_seconds` used for this evaluation               |
    | `metadata.user_page_dwell_canonical_url` | The canonical URL of the trailing streak (truncated to 512 chars)              |

    ### JSON config example

    ```json theme={null}
    {
      "proactive_triggers": {
        "builtins": [
          {
            "id": "user_page_dwell",
            "name": "Page dwell",
            "description": "Fires when the user lingers on a page without doing much"
          }
        ],
        "dwell_threshold_seconds": 90,
        "user_page_dwell_max_actions": 3,
        "dwell_proactive_body": "Taking your time here — can I help with anything?"
      }
    }
    ```
  </Accordion>

  <Accordion title="section_playbook_match — Section-level guidance">
    ### What it detects

    The user is in a part of your product (a "section") that has a matching entry in a guidance playbook you define. The trigger resolves which section the user is currently in, looks up the playbook, and fires with the section-specific copy if a match is found.

    This is the most configurable built-in — it lets you define custom proactive messages for different areas of your product without writing Python code.

    ### Stable ID

    ```python theme={null}
    from autoplay_sdk.proactive_triggers.defaults import TRIGGER_ID_SECTION_PLAYBOOK_MATCH
    # TRIGGER_ID_SECTION_PLAYBOOK_MATCH = "section_playbook_match"
    ```

    ### Configuration inputs

    All inputs are passed via `context_extra` (or `integration_config.proactive_triggers` when using the stock event connector, which copies the keys automatically).

    #### `section_url_rules` — Map URLs to sections

    An ordered array of `{ prefix, section_id }` objects. The first prefix that matches the user's current canonical URL wins. Put longer / more specific prefixes first.

    ```json theme={null}
    {
      "section_url_rules": [
        { "prefix": "/projects/settings", "section_id": "project_settings" },
        { "prefix": "/projects",           "section_id": "projects" },
        { "prefix": "/billing",            "section_id": "billing" }
      ]
    }
    ```

    `section_url_fallback_id` (optional) — bucket ID for URLs that match no prefix. Defaults internally to `"other"` when resolving unmapped paths.

    #### `section_playbook` — Guidance copy per section

    A dict mapping `section_id` to either a plain body string or a structured row:

    ```json theme={null}
    {
      "section_playbook": {
        "projects": "Need help with your projects? I can walk you through it.",
        "billing": {
          "body": "Questions about billing?",
          "reply_option_labels": ["Upgrade plan", "View invoices", "Contact support"]
        }
      }
    }
    ```

    | Row format   | Fields                                                                                              |
    | ------------ | --------------------------------------------------------------------------------------------------- |
    | Plain string | Used directly as the `body`                                                                         |
    | Dict         | `body` or `proactive_body` for the message; `reply_option_labels` or `quick_reply_labels` for chips |

    #### `product_section_playbook` — Section activity data (auto-computed)

    When `section_url_rules` is non-empty, the stock event connector computes this automatically and attaches it to `context_extra`. It contains:

    * `runtime.current_section_id` — the section ID resolved from the user's latest action
    * `sections[section_id].visit_count` — number of times the user visited this section
    * `sections[section_id].dwell_seconds_per_visit` — list of floats, one per visit
    * `sections[section_id].first_visited_at` / `last_visited_at` — ISO 8601 UTC timestamps

    You do not need to set this manually when using the stock connector.

    ### Resolution order

    When multiple sections could match, the trigger picks the section to use in this order:

    1. `runtime.current_section_id` if it exists in `section_playbook` (current page takes priority)
    2. The section with the highest **total dwell time** across all visits, among sections that appear in both `product_section_playbook.sections` and `section_playbook` (tie-break: `visit_count`)
    3. `runtime.current_section_id` if it has a playbook row (fallback)

    ### Result

    | Field                 | Value                                                          |
    | --------------------- | -------------------------------------------------------------- |
    | `trigger_id`          | `"section_playbook_match"`                                     |
    | `body`                | Body string from the matching playbook row                     |
    | `reply_option_labels` | Chip labels from the playbook row (empty tuple if not defined) |
    | `metadata.section_id` | The section ID that matched                                    |

    ### JSON config example

    ```json theme={null}
    {
      "proactive_triggers": {
        "builtins": [
          {
            "id": "section_playbook_match",
            "name": "Section guidance",
            "description": "Fires section-specific guidance based on where the user is"
          }
        ],
        "section_url_rules": [
          { "prefix": "/projects", "section_id": "projects" },
          { "prefix": "/billing",  "section_id": "billing" }
        ],
        "section_url_fallback_id": "general",
        "section_playbook": {
          "projects": "Need help with your projects?",
          "billing": {
            "body": "Questions about billing?",
            "reply_option_labels": ["Upgrade plan", "View invoices"]
          }
        }
      }
    }
    ```
  </Accordion>
</AccordionGroup>

***

## Quick comparison

| Trigger                   | Fires when                                 | Key inputs                                  | Zero-config?                                     |
| ------------------------- | ------------------------------------------ | ------------------------------------------- | ------------------------------------------------ |
| `canonical_url_ping_pong` | User bounces between same URLs             | `canonical_urls` list                       | Yes — active by default                          |
| `user_page_dwell`         | User lingers on one URL with few actions   | `recent_actions`, `dwell_threshold_seconds` | No — must add to `builtins`                      |
| `section_playbook_match`  | User is in a section with a playbook entry | `section_url_rules`, `section_playbook`     | No — must add to `builtins` + configure playbook |

***

## Related pages

* **[Proactive triggers](/sdk/proactive-triggers)** — Module reference: types, constants, registry API, connector JSON schema.
* **[Authoring proactive triggers](/sdk/proactive-triggers-authoring)** — Build custom triggers in code.
* **[Agent session states](/sdk/agent-states)** — FSM gating (`can_show_proactive_with_reason`, idle expiry).
