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

# PostHog

> Use bounded PostHog analytics and behavioral context alongside live operational data through a customer-hosted runner.

The PostHog integration is a read-only runner SDK family for bounded analytics
and operational composition. It can discover events, properties, tables, and
fields, run parameterized HogQL, resolve ephemeral behavioral populations, enrich explicit
external user IDs, and read a compact activity timeline.

It does not manage PostHog dashboards, flags, experiments, or surveys. Use
PostHog directly for those workflows. Polycore is useful when PostHog behavior
needs to be combined with authoritative state from Postgres, Firestore, or an
internal API without syncing those systems into PostHog.

## Architecture

```mermaid theme={"system"}
flowchart LR
  Caller["Agent or dashboard"]
  CP["Polycore control plane"]
  Runner["Runner in your infrastructure"]
  Key["Scoped PostHog read key"]
  PH["PostHog project"]
  Data["Postgres, Firestore, internal APIs"]

  Caller --> CP --> Runner
  Runner --> Key --> PH
  Runner --> Data
```

The PostHog API key remains on the runner host. Query results return through the
normal governed invocation and audit path.

## Configure the family

Commit only the PostHog host and env-var names:

```json polycore.json theme={"system"}
{
  "integrations": {
    "posthog": {
      "host": "https://us.posthog.com",
      "projectIdEnv": "POSTHOG_PROJECT_ID",
      "apiKeyEnv": "POSTHOG_PERSONAL_API_KEY",
      "limits": { "maxLookbackDays": 365 }
    }
  },
  "context": { "file": "./context/product.md" }
}
```

`projectIdEnv`, `apiKeyEnv`, and `limits.maxLookbackDays` default to the
values shown.
EU Cloud uses `https://eu.posthog.com`. Self-hosted installations may use their
own HTTPS origin; HTTP is accepted only for localhost evaluation. The host
cannot contain credentials, a path, query parameters, or a fragment.

Inject runtime values through the runner deployment:

```text theme={"system"}
POSTHOG_PROJECT_ID=12345
POSTHOG_PERSONAL_API_KEY=<read-key>
```

Use a dedicated PostHog key with only the scopes the enabled reads need:

* `query:read` for HogQL and actor-oriented capabilities.
* `event_definition:read` for `posthog.listEvents`.
* `property_definition:read` for `posthog.listProperties`.

The family exposes no provider mutations, so it needs no write scopes.

## Available capabilities

<ResponseField name="posthog.listEvents" type="read">
  Lists up to 500 event definitions with names, descriptions, tags,
  verification, visibility, and last-seen metadata. Supports exact-name
  filtering and bounded offset pagination.
</ResponseField>

<ResponseField name="posthog.listProperties" type="read">
  Lists up to 500 event, person, group, or session property definitions.
  Supports search, event-name filters, and bounded offset pagination.
</ResponseField>

<ResponseField name="posthog.listTables" type="read">
  Lists PostHog core/system tables, imported warehouse tables, saved and
  materialized views, managed views, batch exports, and endpoints. Supports
  search, type filters, and bounded pagination.
</ResponseField>

<ResponseField name="posthog.describeTable" type="read">
  Describes one exact table from `posthog.listTables`, including bounded fields,
  HogQL expressions, serialized types, validity, nested-field hints, row count,
  and catalog certification where available.
</ResponseField>

<ResponseField name="posthog.query" type="read">
  Runs one parameterized HogQL `SELECT` or `WITH` query. Results are capped at
  1,000 rows, 200 columns, and 64 KiB. This is for bounded ad-hoc analytics, not
  event/person exports.
</ResponseField>

<ResponseField name="posthog.segment" type="read">
  Resolves an ephemeral population from event-count criteria and event/person
  property filters. Returns recent distinct-ID aliases, criterion counts, and
  last activity for at most 500 people. It does not persist a PostHog cohort.
</ResponseField>

<ResponseField name="posthog.enrich" type="read">
  Enriches up to 100 explicit external distinct IDs with event counts, active
  days, and first/last matching activity. PostHog's `person_distinct_ids`
  mapping includes history merged from anonymous identifiers.
</ResponseField>

<ResponseField name="posthog.activity" type="read">
  Returns up to 200 recent events for one external distinct ID. Event names and
  returned properties are explicit; `elements_chain` is excluded by default.
</ResponseField>

## Parameterized HogQL

Never interpolate caller-controlled text into HogQL. Use PostHog's constant
placeholders:

```json theme={"system"}
{
  "query": "SELECT event, count() FROM events WHERE distinct_id = {actor} AND timestamp > now() - INTERVAL {days} DAY GROUP BY event",
  "values": {
    "actor": "user_123",
    "days": 30
  },
  "name": "activity_by_event",
  "maxRows": 50
}
```

The runner wraps the query in an outer `LIMIT`, so a larger limit inside the
HogQL cannot bypass the configured result ceiling. PostHog also limits query
execution and concurrency at project level. Include short time windows and
aggregate in PostHog rather than returning raw events.

## Operational composition

### Enrich records from another system

1. Read a bounded account or user list from Postgres/Firestore.
2. Pass its external IDs to `posthog.enrich`.
3. Join the returned behavioral facts into a Polycore page or agent answer.

For example, a renewal queue can add last activity, active days, and key-feature
usage without sending billing or account tables to PostHog.

### Resolve behavior, then read authoritative state

1. Use `posthog.segment` to identify people who performed or missed selected
   behavior.
2. Resolve the returned distinct IDs in the system of record.
3. Present the combined evidence or propose an operation through a separate,
   approval-gated customer action.

Keep lists bounded. For an exact join over thousands of people, create a named
composite read action that performs both reads inside the runner and returns
only aggregates or a small opportunity list.

## Composite read actions

The runner SDK exports `PostHogClient` for reviewed actions that need to combine
PostHog with another local client without routing an intermediate actor set
through the control plane. It exposes the same bounded parameterized HogQL and
taxonomy reads as the built-in family:

```ts theme={"system"}
import { defineAction, PostHogClient, z } from "@polycore/runner";

export default defineAction({
  title: "Behavior and account summary",
  description: "Return one bounded cross-system summary.",
  hazard: "read",
  input: z.object({ distinctId: z.string() }),
  output: z.object({ pageviews: z.number() }),
  secrets: {
    POSTHOG_HOST: "PostHog HTTPS origin",
    POSTHOG_PROJECT_ID: "PostHog project id",
    POSTHOG_API_KEY: "Scoped PostHog read key",
  },
  async run({ input, secrets }) {
    const posthog = new PostHogClient({
      host: secrets.POSTHOG_HOST,
      projectId: secrets.POSTHOG_PROJECT_ID,
      apiKey: secrets.POSTHOG_API_KEY,
    });
    const result = await posthog.runHogQL({
      query:
        "SELECT count() FROM events WHERE event = '$pageview' " +
        "AND distinct_id = {actor} " +
        "AND timestamp > now() - INTERVAL 30 DAY",
      values: { actor: input.distinctId },
      name: "behavior_and_account_summary",
      maxRows: 1,
    });
    return { pageviews: Number(result.rows[0]?.[0] ?? 0) };
  },
});
```

The action should still keep the API key read-only and return a small declared
output. Use the built-in capabilities unless the join itself must stay entirely
inside the runner or is a durable domain operation.

## Identity behavior

`posthog.enrich` and `posthog.activity` resolve an external distinct ID through
PostHog's `person_distinct_ids` table, then query by PostHog person ID. This
includes events captured under anonymous identifiers that PostHog later merged
into the same person.

`posthog.segment` returns all recent distinct-ID aliases observed for each
matched person. The caller should select the identifier used by its system of
record rather than assuming the latest browser identifier is canonical.

## Enforced limits

* Raw HogQL: 20,000 input characters, 1,000 returned rows, 200 columns.
* Event/property definitions and table inventory: 500 per invocation.
* Table description: 1,000 fields with bounded pagination.
* Segment: 10 criteria, 10 property filters per criterion, 500 people.
* Enrichment: 100 distinct IDs and 20 metrics.
* Activity: 200 events, 50 event names, 20 selected properties.
* Every capability result: 64 KiB.
* Upstream HTTP response: 4 MB.
* Default request timeout: 45 seconds.
* Actor lookback: at most `maxLookbackDays` from committed config.

Transient throttles and selected provider failures are retried with a small,
bounded backoff. Requests never follow a provider-supplied URL, and callers
cannot change the configured host or project.

## Production validation

<Check>
  Event/property discovery and table/field introspection return the intended
  project taxonomy and database catalog.
</Check>

<Check>
  A parameter containing quotes and SQL operators round-trips as a value instead
  of changing query structure.
</Check>

<Check>A broad query is truncated below the row and byte limits.</Check>

<Check>
  Segment aliases can be resolved to the identifiers used by the system of
  record.
</Check>

<Check>
  Enrichment includes merged anonymous history for a known identified user.
</Check>

<Check>
  Missing scopes, throttling, malformed provider responses, and timeouts fail
  clearly without logging the API key.
</Check>
