A collection of PostHog skills for enhancing AI-assisted workflows. Add this repo as a Claude Code plugin marketplace to get access to all PostHog skills: Then install individual plugins: Or browse available plugins: Copy any skill directory to .claude/skills/ in your project: Any directory under skills/ that contains a .claude-plugin/plugin.json is automatically discovered and added to the market
Add this skill
npx mdskills install PostHog/exploring-autocapture-events@PostHog? Sign in with GitHub to claim this listing.Comprehensive workflow for exploring PostHog autocapture events with clear steps, examples, and pitfall warnings
1---2name: exploring-autocapture-events3description: >4 Guides exploration of $autocapture events captured by posthog-js to understand user interactions,5 find CSS selectors (especially data-attr attributes), evaluate selector uniqueness, query matching6 clicks ad-hoc, and create actions. Use when the user asks about autocapture data, wants to find7 what users are clicking, needs to build actions from click events, asks about elements_chain,8 wants to build a trend or funnel filtered by clicks or other autocapture interactions, asks which9 properties autocapture sends, or asks how to filter $autocapture events. Only applies to projects10 using posthog-js autocapture.11---1213# Exploring autocapture events1415if users opt in then posthog-js automatically captures clicks, form submissions, and page changes as `$autocapture` events.16Each event records the clicked DOM element and its ancestors in the `elements_chain` column.1718`$autocapture` is intentionally excluded from the `posthog:read-data-schema` taxonomy19because it is only useful with autocapture-specific filters (selector, tag, text, href).20This skill fills that gap.2122## Materialized columns2324The `events` table provides fast access to common element fields without parsing the full chain string.2526| Column | Type | Description |27| ------------------------- | ------------- | ------------------------------------------------------------------------------------------------------ |28| `elements_chain` | String | Full semicolon-separated element chain (see [format reference](./references/elements-chain-format.md)) |29| `elements_chain_href` | String | Last href value from the chain |30| `elements_chain_texts` | Array(String) | All text values from elements |31| `elements_chain_ids` | Array(String) | All id attribute values |32| `elements_chain_elements` | Array(String) | Useful tag names: a, button, input, select, textarea, label |3334Use materialized columns for exploration queries whenever possible — they avoid regex parsing.3536## Canonical autocapture properties3738Every `$autocapture` event from posthog-js ships with a fixed set of properties.39Do not query the schema to "look them up" — they are these:4041| Property | Examples | Notes |42| ----------------- | --------------------------------- | ----------------------------------------------------------- |43| `$event_type` | `click`, `submit`, `change` | the kind of interaction |44| `$el_text` | `Sign up`, `Submit` | text of the clicked element |45| `$current_url` | `https://app.example.com/pricing` | page the interaction happened on |46| `$elements_chain` | semicolon-separated chain | parsed via the `elements_chain*` materialized columns above |4748Standard event properties (`$browser`, `$os`, `$device_type`, etc.) are also present.4950## Workflow5152### 1. Confirm autocapture data exists5354Run a count query before doing anything else.55If the count is zero, autocapture may be disabled. There are two ways this happens:5657- **Project settings** — the team can set `autocapture_opt_out` in PostHog project settings58- **SDK config** — the posthog-js `init()` call can pass `autocapture: false`5960Tell the user if no data is found so they can check both settings.6162```sql63SELECT count() as cnt64FROM events65WHERE event = '$autocapture'66 AND timestamp > now() - INTERVAL 7 DAY67```6869### 2. Explore what users are interacting with7071Start broad using the materialized columns.72The goal is to understand what users are clicking before narrowing down.7374Useful explorations:7576- Top clicked tag names (via `elements_chain_elements`)77- Top clicked text values (via `elements_chain_texts`)78- Top clicked hrefs (via `elements_chain_href`)79- Raw `elements_chain` values for a specific page (filtered by `properties.$current_url`)8081See [example queries](./references/example-queries.md) for all patterns.8283### 3. Find candidate selectors8485Once the user identifies an interaction they care about, find a CSS selector that identifies it.8687Priority order for selector attributes (best first):88891. **`data-attr` or other `data-*` attributes** — highest specificity, stable across deploys, developer-intended anchors.90 Search with `match(elements_chain, 'data-attr=')` or `extractAll`.912. **Element ID** (`attr_id`) — also highly stable, queryable via `elements_chain_ids`.923. **Tag + class combination** — moderately stable but classes change with CSS refactors.934. **Text content** — fragile (changes with copy edits, i18n) but sometimes the only option.945. **Tag name alone** — too broad on its own, useful as a qualifier.9596When a `data-attr` value is found, construct a selector like `[data-attr="value"]` or `button[data-attr="value"]`.9798### 4. Evaluate selector uniqueness99100A selector is only useful if it matches the intended interaction and not unrelated events.101102Run a uniqueness check using `elements_chain =~` with the regex pattern for the selector.103Then sample matching events to inspect what the selector actually captures.104Compare the count against total autocapture volume to understand selectivity.105106A good selector matches a single logical interaction.107If it matches too many distinct elements, refine it in the next step.108109### 5. Refine with additional filters110111If the selector alone is not unique enough, layer on additional filters:112113- **Text filter** — match by element text content using `elements_chain_texts`114- **URL filter** — restrict to a specific page using `properties.$current_url`115- **Href filter** — match by link target using `elements_chain_href`116117Re-run the uniqueness check after each refinement.118Only include filters that are needed — fewer filters means more resilience to minor DOM changes.119120### 6. Filter autocapture inside an insight query121122When the user wants a funnel, trend, or other insight, the filter shape is different from HogQL.123Each step in a `FunnelsQuery` / `TrendsQuery` is an `EventsNode` (or `ActionsNode`) with `event: "$autocapture"` and a `properties` array.124125Two distinct property `type` values matter — they are not interchangeable:126127- **`type: "element"`** — keys: `selector`, `tag_name`, `text`, `href`. Matched against the parsed `elements_chain`. Operator support is split:128 - `selector` and `tag_name` only support `exact` and `is_not` — anything else raises `NotImplementedError` in the query compiler (`posthog/hogql/property.py`).129 - `text` and `href` accept the full string operator set (`exact`, `is_not`, `icontains`, `not_icontains`, `regex`, `not_regex`, `is_set`, `is_not_set`).130- **`type: "event"`** — keys: any of the canonical autocapture properties (`$event_type`, `$el_text`, `$current_url`) or anything else on the event. Standard event-property operators (`exact`, `icontains`, `regex`, etc.).131132Example funnel from clicking one button to clicking another:133134```json135{136 "kind": "FunnelsQuery",137 "series": [138 {139 "kind": "EventsNode",140 "event": "$autocapture",141 "properties": [142 {143 "type": "element",144 "key": "selector",145 "value": ["[data-attr=\"autocapture-series-save-as-action-banner-shown\"]"],146 "operator": "exact"147 }148 ]149 },150 {151 "kind": "EventsNode",152 "event": "$autocapture",153 "properties": [154 {155 "type": "element",156 "key": "selector",157 "value": ["[data-attr=\"autocapture-save-as-action\"]"],158 "operator": "exact"159 }160 ]161 }162 ]163}164```165166Two things easy to get wrong:167168- `value` is an array even when matching a single selector169- The selector string includes the `[data-attr="..."]` wrapper — it is a CSS selector, not a bare attribute value170171Decision rule: prefer an action (`ActionsNode` referencing an existing action — see Step 8) when the interaction will be referenced more than once; inline `type: "element"` / `type: "event"` filters when it's a one-off insight; raw HogQL (Step 7) when joining across events or doing custom aggregations.172173### 7. Use in ad-hoc queries174175The discovered selector can be used directly in HogQL without creating an action.176177**Trends** — count matching clicks over time:178179```sql180SELECT181 toStartOfDay(timestamp) as day,182 count() as clicks183FROM events184WHERE event = '$autocapture'185 AND timestamp > now() - INTERVAL 14 DAY186 AND elements_chain =~ '(^|;)button.*?data-attr="checkout"'187GROUP BY day188ORDER BY day189```190191**Funnel** — pageview to click conversion:192193```sql194SELECT195 person_id,196 first_pageview,197 first_click_after198FROM (199 SELECT200 p.person_id,201 p.pageview_time as first_pageview,202 min(c.click_time) as first_click_after203 FROM (204 SELECT person_id, min(timestamp) as pageview_time205 FROM events206 WHERE event = '$pageview'207 AND timestamp > now() - INTERVAL 14 DAY208 AND properties.$current_url ILIKE '%/pricing%'209 GROUP BY person_id210 ) p211 INNER JOIN (212 SELECT person_id, timestamp as click_time213 FROM events214 WHERE event = '$autocapture'215 AND timestamp > now() - INTERVAL 14 DAY216 AND elements_chain =~ '(^|;)button.*?data-attr="signup"'217 ) c ON p.person_id = c.person_id AND c.click_time > p.pageview_time218 GROUP BY p.person_id, p.pageview_time219)220```221222For recurring analysis, prefer creating an action (next step) or using `posthog:query-trends` / `posthog:query-funnel` with the action.223224### 8. Create an action225226Actions are the durable version of ad-hoc selector queries.227Once the criteria uniquely identify the interaction, create an action using `posthog:action-create`.228229Construct the step with only the filters needed for uniqueness:230231```json232{233 "name": "Clicked checkout button",234 "steps": [235 {236 "event": "$autocapture",237 "selector": "button[data-attr='checkout']",238 "text": "Complete Purchase",239 "text_matching": "exact",240 "url": "/checkout",241 "url_matching": "contains"242 }243 ]244}245```246247Available step fields for `$autocapture`:248249- `selector` — CSS selector (e.g. `button[data-attr='checkout']`)250- `tag_name` — HTML tag name (e.g. `button`, `a`, `input`)251- `text` / `text_matching` — element text (`exact`, `contains`, or `regex`)252- `href` / `href_matching` — link href (`exact`, `contains`, or `regex`)253- `url` / `url_matching` — page URL (`exact`, `contains`, or `regex`)254255After creation, verify with `matchesAction()`:256257```sql258SELECT count() as matching_events259FROM events260WHERE matchesAction('Clicked checkout button')261 AND timestamp > now() - INTERVAL 7 DAY262```263264## Tips265266- Always set timestamp filters — `$autocapture` is high volume267- Use `LIMIT` generously when sampling `elements_chain` — the strings can be long268- The `elements_chain =~` operator matches CSS selectors as regex internally;269 prefer materialized columns when possible for performance270- This workflow only applies to posthog-js — other SDKs do not capture elements271
Full transparency — inspect the skill content before installing.