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

# Proactive triggers

> Detect the right moment to surface proactive assistance — without coupling to any chat vendor or UI layer.

Proactive triggers let your app **decide when to reach out to a user** — before they ask for help. A trigger watches the stream of user actions, evaluates a condition, and returns a message body (and optional quick-reply labels) when that condition is met. The rest of your stack (Intercom, a modal, a toast) handles delivery.

> **New to this?** Start with [Authoring proactive triggers](/sdk/proactive-triggers-authoring) for a step-by-step walkthrough of building your first trigger.

***

## How it works in three steps

```
User actions → ProactiveTriggerContext → ProactiveTriggerRegistry.evaluate_first()
                                                        ↓
                                          ProactiveTriggerResult  (or None)
                                                        ↓
                                              Your delivery layer
                                         (Intercom quick_reply, modal, etc.)
```

1. **Build context** — On each tick, construct a `ProactiveTriggerContext` from your event buffer (recent actions, canonical URLs, session summary, etc.).
2. **Evaluate** — Call `registry.evaluate_first(ctx)`. The registry walks its triggers in priority order and returns the first `ProactiveTriggerResult` whose predicate matches, or `None`.
3. **Deliver** — Pass the result's `body` and optional `reply_option_labels` to your delivery layer. Gate on `SessionState` (v2) before sending.

<Note>
  **Two ways to act on a trigger**

  * **Visual guidance (available now)** — surface a chat message, quick reply, modal, or in-app tour via your delivery layer (Intercom, a custom UI, etc.)
  * **Browser agent triggering (coming soon)** — fire an autonomous browser agent that completes the task on the user's behalf — no manual steps required.
</Note>

***

## Quickstart

```python theme={null}
from autoplay_sdk.proactive_triggers import (
    PredicateProactiveTrigger,
    ProactiveTriggerContext,
    ProactiveTriggerRegistry,
)

# 1. Define a trigger
trigger = PredicateProactiveTrigger(
    trigger_id="high_action_volume",
    body="Looks like you've been busy — need a hand?",
    predicate=lambda ctx: ctx.action_count >= 12,
)

# 2. Register it
registry = ProactiveTriggerRegistry([trigger])

# 3. Build context and evaluate on each tick
ctx = ProactiveTriggerContext(
    session_id="sess_abc",
    product_id="prod_xyz",
    action_count=14,
    # ... other fields
)

result = registry.evaluate_first(ctx)

if result:
    print(result.trigger_id)   # "high_action_volume"
    print(result.body)          # "Looks like you've been busy — need a hand?"
```

***

## Module: `autoplay_sdk.proactive_triggers`

### Core types

| Class / function            | Purpose                                                                                      |
| --------------------------- | -------------------------------------------------------------------------------------------- |
| `ProactiveTriggerContext`   | Snapshot of session state passed to every trigger on each tick                               |
| `ProactiveTriggerRegistry`  | Ordered list of triggers; call `evaluate_first(ctx)` to get the first match                  |
| `ProactiveTriggerResult`    | What a firing trigger returns: `trigger_id`, `body`, optional `reply_option_labels`, timings |
| `PredicateProactiveTrigger` | Fast-path trigger for a single boolean condition — no subclassing needed                     |
| `ProactiveTriggerEntity`    | Wraps any trigger to attach explicit `ProactiveTriggerTimings`                               |
| `ProactiveTriggerTimings`   | Holds `interaction_timeout_s` and `cooldown_s`                                               |

### Proactive data structures (full map)

| Structure                                              | Layer             | Purpose                                                   | Typical source                             |
| ------------------------------------------------------ | ----------------- | --------------------------------------------------------- | ------------------------------------------ |
| `ProactiveTriggerConfig`                               | proactive config  | One trigger definition (`id`, `name`, criteria, messages) | `integration_config.proactive_intercom[*]` |
| `TriggerMessage`                                       | proactive config  | Quick-reply/chip option shown to the user                 | `ProactiveTriggerConfig.messages`          |
| `CriteriaLeaf` / `CriteriaGroup` (`ProactiveCriteria`) | proactive config  | Recursive rule tree (`AND`/`OR` groups)                   | `proactive_criteria` JSON                  |
| `ProactiveTriggerContext`                              | proactive runtime | Evaluation input snapshot for one tick                    | Built from recent actions/summaries        |
| `ProactiveTriggerResult`                               | proactive runtime | Firing result (`body`, labels, timings, metadata)         | Returned by trigger evaluation             |
| `ProactiveTriggerTimings`                              | proactive policy  | Idle timeout + refire cooldown policy                     | Trigger defaults or overrides              |
| `ProactiveTriggerEntity`                               | proactive runtime | Decorator to enforce explicit timings on a trigger        | Registry construction                      |
| `TourDefinition`                                       | tour registry     | One visual-guidance tour + timing overrides               | `integration_config.tour_registry[*]`      |
| `TourRegistry`                                         | tour registry     | Product-level lookup for `TourDefinition`                 | Parsed once per product config             |
| `SessionState`                                         | agent state v2    | Primary routing and gating state keyed by `session_id`    | Session-state persistence layer            |

### Default timing constants

These live in `autoplay_sdk.proactive_triggers.types` and apply whenever you don't override them:

| Constant                                | Default   | What it controls                                          |
| --------------------------------------- | --------- | --------------------------------------------------------- |
| `DEFAULT_INTERACTION_TIMEOUT_S`         | **10 s**  | How long the proactive UI stays open waiting for the user |
| `DEFAULT_COOLDOWN_S`                    | **30 s**  | Minimum gap before the same trigger may fire again        |
| `DEFAULT_PROACTIVE_CONTEXT_LOOKBACK_S`  | **120 s** | Action window used by `from_actions_payloads` factory     |
| `DEFAULT_PROACTIVE_CONTEXT_MAX_ACTIONS` | **50**    | Max actions loaded by `from_actions_payloads` factory     |

### Custom payload sources (`RecentActionsPayloadSource`)

Implement `RecentActionsPayloadSource` to load `ActionsPayload` batches from any backing store (database, Redis, etc.). Call `build_proactive_context_from_payloads(...)` with the same keyword arguments you would pass to `ProactiveTriggerContext.from_actions_payloads` — it is a thin wrapper for a single import site.

### Monitor cadence and context source

Poll cadence (**\~10 s** proactive monitor default in the stock connector) is separate from the **\~120 s** action lookback window used to build `ProactiveTriggerContext`.

Your host/connector can source recent action history from any durable memory backend or an in-process store. Regardless of source, keep the same contract:

* build `ProactiveTriggerContext` from a recent, ordered action slice,
* run proactive delivery evaluation only when `SessionState` is eligible for unsolicited help (effectively `THINKING`),
* continue running idle-expiry checks for already-open proactive UI on each monitor sweep.

### Built-in trigger IDs

Stable `trigger_id` strings are exported from `autoplay_sdk.proactive_triggers.defaults`:

| Constant                             | Value                       | Trigger class              |
| ------------------------------------ | --------------------------- | -------------------------- |
| `TRIGGER_ID_CANONICAL_URL_PING_PONG` | `"canonical_url_ping_pong"` | `CanonicalPingPongTrigger` |
| `TRIGGER_ID_USER_PAGE_DWELL`         | `"user_page_dwell"`         | `UserPageDwellTrigger`     |
| `TRIGGER_ID_SECTION_PLAYBOOK_MATCH`  | `"section_playbook_match"`  | `SectionPlaybookTrigger`   |

`default_proactive_trigger_registry()` returns a registry pre-loaded with `CanonicalPingPongTrigger` only. To use other built-ins, pass an explicit `builtins` list (see [Event connector JSON](#event-connector-json-builtins) below). Use this as a starting point and append your own triggers.

`get_proactive_trigger_ids()` and `list_builtin_trigger_catalog()` enumerate all available built-in IDs and their catalog metadata at runtime.

### `user_page_dwell` — time on page + sparse actions

The **`user_page_dwell`** built-in looks at the **trailing run** of `recent_actions` that share the **same non-empty `canonical_url` as the latest action** (the “current page” streak). It fires only when **all** of the following hold:

1. **Dwell time** — The time span of that streak is at least **`dwell_threshold_seconds`**.
   * **Default: 60** (one minute). This is the minimum time the user must stay on the same canonical URL before the trigger can match.
2. **Sparse actions** — The number of actions in that streak is at most **`user_page_dwell_max_actions`**.
   * **Default: 5**. If the user has more than this many actions on the same URL streak, the trigger does **not** fire (treats it as active exploration, not passive linger).

Pass tuning values via **`ProactiveTriggerContext.context_extra`** (or, with the stock event connector, under **`integration_config.proactive_triggers`** — see below):

| Key                           | Default                    | Description                                                                                                            |
| ----------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `dwell_threshold_seconds`     | **60**                     | Minimum seconds on the same canonical URL streak (`float`; must be > 0).                                               |
| `user_page_dwell_max_actions` | **5**                      | Maximum actions allowed on that streak for the trigger to fire (`int`; invalid or ≤ 0 falls back to **5**).            |
| `dwell_proactive_body`        | *(built-in short message)* | Optional override for the proactive **body** text when the trigger fires (still capped at `PROACTIVE_BODY_MAX_CHARS`). |

Optional test-only key: **`eval_now`** (Unix time as `float`) — fixes “now” when unit-testing dwell duration.

Fired results include metadata such as `user_page_dwell_seconds`, `user_page_dwell_action_count`, and `user_page_dwell_max_actions` for analytics and debugging.

### Section playbook — `product_section_playbook` + `section_playbook`

The **`section_playbook_match`** built-in selects guidance from **`section_playbook`** (section id → message body or structured row) using section intelligence gathered into **`product_section_playbook`**:

* **`integration_config.proactive_triggers.section_url_rules`** — ordered array of `{ "prefix": "<canonical_url_prefix>", "section_id": "<stable_id>" }`. First matching prefix wins (put longer/more specific prefixes first).
* **`section_url_fallback_id`** — optional bucket id for URLs that match no prefix (defaults internally to **`other`** when resolving unmapped paths).

With the stock event connector, when **`section_url_rules`** is non-empty, **`build_proactive_trigger_context_for_session`** attaches **`product_section_playbook`** to **`context_extra`**: **`runtime.current_section_id`** (latest action’s resolved section) and **`sections[section_id]`** with **`visit_count`**, **`dwell_seconds_per_visit`** (one float per visit), **`first_visited_at`**, **`last_visited_at`** (ISO 8601 UTC).

**Resolution order** for which playbook row fires: non-empty **`current_section_id`** if it exists in **`section_playbook`** (host override); else highest total dwell among sections that appear in both **`product_section_playbook.sections`** and **`section_playbook`** (tie-break **`visit_count`**); else **`runtime.current_section_id`** if it has a playbook row.

The LLM judge receives both **`product_section_playbook`** and **`section_playbook`** inside **`context_extra_json`** when present.

***

## `ProactiveTriggerContext` fields

| Field                     | Type                     | Required | Description                                                                                                                                                                        |
| ------------------------- | ------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session_id`              | `str`                    | ✅        | Identifies the user session                                                                                                                                                        |
| `product_id`              | `str`                    | ✅        | Identifies the product / tenant                                                                                                                                                    |
| `conversation_id`         | `str \| None`            | —        | Chat thread ID (aligns cooldown keys with the chat surface)                                                                                                                        |
| `action_count`            | `int`                    | —        | Total actions seen this session                                                                                                                                                    |
| `canonical_urls`          | `list[str \| None]`      | —        | Chronological page URLs — used by `CanonicalPingPongTrigger`                                                                                                                       |
| `recent_actions`          | `tuple[SlimAction, ...]` | —        | Slice of recent actions from your buffer                                                                                                                                           |
| `latest_summary_text`     | `str \| None`            | —        | Latest session summary from your summarizer                                                                                                                                        |
| `prior_session_summaries` | `tuple[str, ...]`        | —        | Past session summaries (from Redis, DB, etc.)                                                                                                                                      |
| `context_extra`           | `dict`                   | —        | Connector-specific data (e.g. `dwell_threshold_seconds`, `user_page_dwell_max_actions`, `section_playbook`, `product_section_playbook`, `section_url_rules`, `current_section_id`) |

`session_id` and `product_id` are validated at construction under `ScopePolicy.STRICT` (default) — both must be non-empty strings. Use `ScopePolicy.LENIENT` only at legacy call sites.

***

## Event connector JSON (`builtins`)

If you're configuring triggers via `integration_config.proactive_triggers.builtins` (the JSON path used by the stock event connector), each row **must** include:

```json theme={null}
{
  "proactive_triggers": {
    "builtins": [
      {
        "id": "canonical_url_ping_pong",
        "name": "URL hesitation",
        "description": "Fires when the user bounces between the same URLs"
      }
    ]
  }
}
```

* `id` must match an entry in the built-in catalog (see `list_builtin_trigger_catalog()`).
* `name` and `description` are required non-empty strings — used for display, analytics, and admin UIs.
* `interaction_timeout_s` and `cooldown_s` are optional; omit to use the catalog's defaults.
* `registry` must be omitted or `"default"`.
* `mode: "ping_pong_only"` restricts results to `canonical_url_ping_pong` only.
* Invalid rows raise `SdkConfigError` and log `event=proactive_builtin_spec_invalid`.

The stock connector only loads triggers from the SDK catalog via JSON — arbitrary code cannot be injected from config.

**Tuning from product config** — the event connector copies keys from **`integration_config.proactive_triggers`** into `context_extra`, including: `dwell_threshold_seconds`, `user_page_dwell_max_actions`, `dwell_proactive_body`, `section_playbook`, `section_url_rules`, `section_url_fallback_id`, `current_section_id`, and (when rules exist) computed **`product_section_playbook`**.

***

## When a user chooses a proactive option

If the proactive message includes quick replies (`TriggerMessage` rows), this is the canonical structure flow:

1. Delivery layer receives selected chip/option id from the user.
2. Resolve selected id to `TriggerMessage`.
3. Update `SessionState` interaction path (for example, record option interaction and keep session routing state current).
4. If `TriggerMessage.user_tour_exists` is true, resolve tour metadata in `TourRegistry`:
   * by chip id (`TourDefinition.id`) when ids are aligned, or
   * by `user_tour_id` (`TourRegistry.get_by_user_tour_id(...)`) when using provider flow IDs.
5. Start visual guidance and continue timeout/cooldown handling using session state + tour timing rules.

This keeps proactive choice handling deterministic and data-structure driven, with `session_id` as primary scope.

***

## Related pages

* **[Authoring proactive triggers](/sdk/proactive-triggers-authoring)** — Step-by-step: build a trigger, build context, register and evaluate, handle delivery.
* **[Agent session states](/sdk/agent-states)** — `SessionState` v2 gating and idle expiry behavior.
* **[Intercom integration](/integrations/intercom)** — `quick_reply` HTTP shape, delete-conversation helpers.
* **[Typed payloads](/sdk/typed-payloads)** — `SlimAction`, `ActionsPayload`, `SummaryPayload`.
