Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions transformation-config/skills/posthog-docs/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
type: docs-only
template: description.md
description: Authoring guidance for PostHog documentation – style, formatting, MDX components, and content-type patterns
tags: [posthog-docs]
shared_docs:
- https://posthog.com/handbook/docs-and-wizard/docs-style-guide.md
- https://posthog.com/handbook/docs-and-wizard/mdx-and-components.md
- https://posthog.com/handbook/docs-and-wizard/writing-product-docs.md
- https://posthog.com/handbook/docs-and-wizard/onboarding-docs.md
- https://posthog.com/handbook/docs-and-wizard/sdk-reference-docs.md
- https://posthog.com/handbook/docs-and-wizard/api-specifications.md
variants:
- id: posthog-docs
display_name: PostHog docs authoring
tags: [posthog-docs]
docs_urls: []
91 changes: 91 additions & 0 deletions transformation-config/skills/posthog-docs/description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# PostHog docs authoring

This skill helps you write and edit documentation that lives on posthog.com. It packages PostHog's official docs handbook into a single source of truth so what you produce matches what the docs team expects.

## When to use this skill

Auto-trigger when:

- Editing or creating any file under `contents/docs/` in the posthog.com repo
- Editing or creating files under `docs/onboarding/` or `docs/published/` in the posthog/posthog monorepo
- The user mentions writing or editing PostHog docs, a product doc, an SDK reference, an API spec, an onboarding guide, "docs for X product", or "the PostHog docs"

Do **not** trigger for:

- Blog posts, tutorials, handbook pages, or marketing pages on posthog.com (those follow PostHog's general content style guide, not the docs style guide)
- Internal CLAUDE.md or AGENTS.md files
- Engineering RFCs, READMEs, or code comments

## Universal rules (apply to every PostHog doc)

These are the rules that come up most often. Follow them without needing to load any reference file.

1. **Address the reader as "you".** Never "the user", "developers", or "we".
2. **Active voice, present tense.** "PostHog captures events" – not "events will be captured by PostHog".
3. **Sentence case for headings.** "How to create a feature flag" – not "How To Create A Feature Flag".
4. **American English** with the **Oxford comma**.
5. **British-style en dash with spaces** ( – ), not em dash. On Mac: Option + hyphen.
6. **Use straight quotes and apostrophes**, never curly ones.
7. **Capitalize PostHog product names** as proper nouns (Product Analytics, Session Replay, Feature Flags).
8. **Never** call the project token an "API key". The **project token** (`phs_...`) is public and goes in SDKs. The **personal API key** (`phx_...`) is private and for server-side API access.
9. **Default to "PostHog"**, not "PostHog Cloud" – only specify "Cloud" when contrasting with self-hosted.
10. **Avoid trivializing words**: don't write "simply", "just", "easily", "obviously", "of course", "clearly".
11. **Use relative URLs for internal links** (`/docs/feature-flags`, not `https://posthog.com/docs/feature-flags`).
12. **Bold UI elements** instead of quoting them: Click **New insight**.
13. **`snake_case` for all PostHog event and property names** in code examples. Never camelCase or PascalCase.
14. **Don't use stock Tailwind colors** in any embedded styles – only PostHog tokens.

If you violate any of the above, fix it before considering a doc done.

## Where docs live (orientation)

PostHog docs are split between two repos. Knowing where content lives prevents wasted edits.

- **posthog.com repo (`contents/docs/`)** – most product docs, marketing-adjacent docs, hand-rolled API overviews, blog posts, tutorials, the non-engineering handbook.
- **posthog/posthog monorepo (`docs/published/`)** – engineering handbook pages and product docs that are tightly coupled to monorepo code (e.g., surveys SDK feature support).
- **posthog/posthog monorepo (`docs/onboarding/`)** – the **single source** for in-app installation/onboarding instructions. The website pulls these in automatically. Do not duplicate this content in the website repo. Use the onboarding-docs.md reference for onboarding docs.

## Reference files (load on demand)

The files below are deep references. Load the ones relevant to the current task – don't load all of them up front.

| File | When to load |
|------|--------------|
| [references/style-guide.md](references/style-guide.md) | **Always load** for any docs writing or editing task. Covers voice, grammar, formatting, code conventions, links, screenshots, and word choice. |
| [references/mdx-and-components.md](references/mdx-and-components.md) | Load when working in any `.mdx` file under `contents/docs/`. Covers frontmatter, snippets, magic placeholders like `<ph_project_token>`, and the full component library (`<CalloutBox>`, `<Steps>`, `<ProductScreenshot>`, `<MultiLanguage>`, etc.). |
| [references/product-docs.md](references/product-docs.md) | Load when creating or restructuring a **product's docs section** (Overview, Getting started, Concepts, Guides, PostHog AI, Resources). Use Error Tracking docs as the reference template. |
| [references/onboarding-docs.md](references/onboarding-docs.md) | Load only when creating, migrating, or modifying **shared in-app onboarding/installation content** (the stuff that renders both in-app and on the website). |
| [references/sdk-reference.md](references/sdk-reference.md) | Load only when working on **SDK reference docs** (the auto-generated method/class/type references stored as HogRef JSON in each SDK repo). |
| [references/api-specs.md](references/api-specs.md) | Load only when working on **API specifications** under `/docs/api/` – particularly when figuring out whether a page is hand-rolled or generated, or how to update OpenAPI-driven endpoints. |

## Quick decision tree

- *"Edit a typo or sentence in an existing doc"* – universal rules above are usually enough. Load `style-guide.md` if the change involves rewording.
- *"Add a callout, step list, or screenshot to a doc"* – load `style-guide.md` + `mdx-and-components.md`.
- *"Write a new product doc page"* (e.g., a guide, concept, or troubleshooting page) – load `style-guide.md` + `mdx-and-components.md` + `product-docs.md`.
- *"Add or change an installation page for a product"* – load `onboarding-docs.md` first to confirm whether the product uses the shared rendering pattern. If it does, the source lives in the monorepo, not here.
- *"Add or update an SDK reference"* – load `sdk-reference.md`. The source likely lives in the SDK repo, not posthog.com.
- *"Document a new API endpoint"* – load `api-specs.md`. The source likely lives in the posthog/posthog repo (Django serializers + `@validated_request`), not posthog.com.

## Tooling

PostHog enforces style through three tools, in roughly this order:

1. **Vale** – prose linter that runs in PRs and catches style guide violations.
2. **InKeep docs writer** – AI agent that uses these style guides as context when drafting docs PRs.
3. **This skill** – agent skill for use in Claude Code and similar tools.

Before opening a PR, run:

```bash
pnpm format:docs
```

to auto-fix MDX whitespace issues that commonly break the parser.

## Boundaries

- **Never** invent components. If you're not sure a component exists, check `src/components/` in posthog.com or look at how existing docs use it. If still unsure, ask.
- **Never** move or rename a published doc page without adding a redirect in `vercel.json`.
- **Never** edit the website's installation MDX stubs to change content – change the source in the monorepo.
- **Never** copy patterns from a single page and assume they're the standard. Cross-reference at least two examples or check the relevant reference file.
245 changes: 245 additions & 0 deletions transformation-config/skills/posthog-docs/references/api-specs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
# API specifications

This file covers PostHog's **API specification docs** under `/docs/api/`. Load it only when you're working on API spec pages – particularly to figure out whether a page is hand-rolled or auto-generated, or how to update an OpenAPI-driven endpoint.

PostHog's API specifications are (mostly) generated automatically from the OpenAPI spec. There's tooling to generate the API specification markdown files from the OpenAPI spec.

---

## Where API specs are published

When you run the PostHog app locally, the API spec is available at [`/api/schema/`](https://app.posthog.com/api/schema/), and you can browse it via [Swagger UI](https://app.posthog.com/api/schema/swagger-ui/).

On the website, API specs live at [`/docs/api/`](https://posthog.com/docs/api). **Some pages are hand-rolled, some are generated.** Knowing which is which determines how you make a change.

| Page | Type |
|------|------|
| [Overview](https://posthog.com/docs/api) | hand-rolled |
| [Capture](https://posthog.com/docs/api/capture) | hand-rolled |
| [Flags](https://posthog.com/docs/api/flags) | hand-rolled |
| [Queries](https://posthog.com/docs/api/queries) | hand-rolled |
| [Actions](https://posthog.com/docs/api/actions) | generated |
| [Alerts](https://posthog.com/docs/api/alerts) | generated |
| [Activity log](https://posthog.com/docs/api/activity-log) | generated |
| [Annotations](https://posthog.com/docs/api/annotations) | generated |
| [Batch exports](https://posthog.com/docs/api/batch-exports) | generated |
| [Cohorts](https://posthog.com/docs/api/cohorts) | generated |
| [Dashboards](https://posthog.com/docs/api/dashboards) | generated |
| [Dashboard templates](https://posthog.com/docs/api/dashboard-templates) | generated |
| [Early access features](https://posthog.com/docs/api/early-access-feature) | generated |
| [Endpoints](https://posthog.com/docs/api/endpoints) | generated |
| [Environments](https://posthog.com/docs/api/environments) | generated |
| [Event definitions](https://posthog.com/docs/api/event-definitions) | generated |
| [Events](https://posthog.com/docs/api/events) | generated |
| [Experiments](https://posthog.com/docs/api/experiments) | generated |
| [Feature flags](https://posthog.com/docs/api/feature-flags) | generated |
| [Groups](https://posthog.com/docs/api/groups) | generated |
| [Groups types](https://posthog.com/docs/api/groups-types) | generated |
| [Hog functions](https://posthog.com/docs/api/hog-functions) | generated |
| [Insights](https://posthog.com/docs/api/insights) | generated |
| [Invites](https://posthog.com/docs/api/invites) | generated |
| [Members](https://posthog.com/docs/api/members) | generated |
| [Notebooks](https://posthog.com/docs/api/notebooks) | generated |
| [Organizations](https://posthog.com/docs/api/organizations) | generated |
| [Persons](https://posthog.com/docs/api/persons) | generated |
| [Projects](https://posthog.com/docs/api/projects) | generated |
| [Property definitions](https://posthog.com/docs/api/property-definitions) | generated |
| [Query](https://posthog.com/docs/api/query) | generated |
| [Roles](https://posthog.com/docs/api/roles) | generated |
| [Session recordings](https://posthog.com/docs/api/session-recordings) | generated |
| [Session recording playlists](https://posthog.com/docs/api/session-recording-playlists) | generated |
| [Sessions](https://posthog.com/docs/api/sessions) | generated |
| [Subscriptions](https://posthog.com/docs/api/subscriptions) | generated |
| [Surveys](https://posthog.com/docs/api/surveys) | generated |
| [Users](https://posthog.com/docs/api/users) | generated |
| [Web Analytics](https://posthog.com/docs/api/web-analytics) | generated |

---

## How the website ingests the OpenAPI spec

The website ingests the OpenAPI spec during the Gatsby build process in two stages:

1. **During `sourceNodes`**: the OpenAPI spec is fetched and parsed using `OpenAPIParser` and `MenuBuilder` from the `redoc` library. This creates a structured menu of API endpoints used for navigation. The menu groups endpoints and handles pagination for groups with more than 20 items.
2. **During `onPostBuild`**: the build fetches the OpenAPI spec from `https://app.posthog.com/api/schema/` (or from `POSTHOG_OPEN_API_SPEC_URL` if set). The spec is then passed to `generateApiSpecMarkdown()`, which:
- Iterates through all paths and HTTP methods in the spec
- For each endpoint with an `operationId`, creates a markdown file named after the operation ID
- Recursively extracts all referenced component schemas for each endpoint
- Generates markdown files containing the endpoint's OpenAPI JSON in a code block
- Writes them to `public/docs/open-api-spec/`

The generated markdown files are then available at `/docs/open-api-spec/{operationId}.md` and are included in the docs site's API reference section.

---

## How to update the OpenAPI spec

For automatically generated pages, the source content comes from the OpenAPI spec. To update them, you typically need to update the spec – which means editing the [`PostHog/posthog`](https://github.com/PostHog/posthog) repository.

### Updating the page title and description

> These updates happen in the **`PostHog/posthog.com`** repository.

**Page title**: update the `titleMap` object in [`src/templates/ApiEndpoint.tsx`](https://github.com/PostHog/posthog.com/blob/master/src/templates/ApiEndpoint.tsx#L85-L120). For example, to change the "Actions" page title, modify the `actions` entry in the map.

**Page description**: create or update an `overview.mdx` file in the corresponding API folder. The file should be at `contents/docs/api/{name}/overview.mdx`, where `{name}` matches the API endpoint name (e.g., `events`, `feature-flags`).

> Example: [`contents/docs/api/events/overview.mdx`](https://github.com/PostHog/posthog.com/blob/master/contents/docs/api/events/overview.mdx) contains the description that appears at the top of the Events API page.

### Updating the endpoint title and description

> These updates happen in the **`PostHog/posthog`** repository.

**Endpoint title**: the title is auto-generated from the `operationId` in the OpenAPI spec via the `generateName()` function in [`src/templates/ApiEndpoint.tsx`](https://github.com/PostHog/posthog.com/blob/master/src/templates/ApiEndpoint.tsx#L128-L137). To customize it, update the `operationId` or description in the Django viewset in the PostHog repo. You essentially update the path to update the title.

**Endpoint description**: create an MDX file named after the endpoint's `operationId` in the appropriate API folder, at `contents/docs/api/{name}/{operationId}.mdx`.

> Example: [`contents/docs/api/feature-flags/feature_flags_list.mdx`](https://github.com/PostHog/posthog.com/blob/master/contents/docs/api/feature-flags/feature_flags_list.mdx) adds custom content that appears under the "List all feature flags" endpoint. Content from this file renders **above** the endpoint's description from the OpenAPI spec.

### Updating endpoint parameters and responses

The endpoint request body parameters, query parameters, path parameters, response body, response headers, API key scopes, etc. are all defined in the Django serializers and viewsets in the PostHog repo.

There are two types of "views" in Django, and they need different annotations to generate accurate OpenAPI specs:

1. **Model-based CRUD views** – backed by Django models. They map literally to model fields and generally don't need extra annotations for accurate request/response definitions.
2. **Function-based views** – backed by Python functions, generally annotated with `@action` decorators. These need manual annotation for request/response definitions to be accurate.

If an endpoint needs additional annotation, use the `@validated_request` decorator. It uses the serializers passed in for **both** validation and OpenAPI annotation, ensuring the OpenAPI spec stays accurate (or that we know when it's not).

#### Basic usage

```python
from posthog.api.mixins import validated_request
from drf_spectacular.utils import OpenApiResponse
from rest_framework import serializers, status
from rest_framework.response import Response

# Django uses serializer to validate request body data, validated request can infer the request and response schemas from the serializer definitions.
class EventCaptureRequestSerializer(serializers.Serializer):
event = serializers.CharField(max_length=200, help_text="Event name")
distinct_id = serializers.CharField(max_length=200, help_text="User distinct ID")
properties = serializers.DictField(required=False, default=dict)

class EventCaptureResponseSerializer(serializers.Serializer):
status = serializers.ChoiceField(choices=["ok", "queued"])
event_id = serializers.UUIDField()
distinct_id = serializers.CharField()

@validated_request(
request_serializer=EventCaptureRequestSerializer,
responses={
200: OpenApiResponse(response=EventCaptureResponseSerializer),
},
summary="Capture an event",
description="Sends an event to PostHog for tracking",
)
def capture_event(self, request):
# Access validated request body data
event_name = request.validated_data["event"]
distinct_id = request.validated_data["distinct_id"]

# Process the event...

return Response(
{
"status": "ok",
"event_id": str(uuid.uuid4()),
"distinct_id": distinct_id,
},
status=status.HTTP_200_OK,
)
```

#### Validating query parameters

Use `query_serializer`:

```python
class QueryParamSerializer(serializers.Serializer):
page = serializers.IntegerField(required=False, default=1)
limit = serializers.IntegerField(required=False, default=10, max_value=100)
include_deleted = serializers.BooleanField(required=False, default=False)

@validated_request(
query_serializer=QueryParamSerializer,
responses={
200: OpenApiResponse(response=ListResponseSerializer),
},
)
def list_items(self, request):
page = request.validated_query_data["page"]
limit = request.validated_query_data["limit"]
return Response(...)
```

#### Multiple response status codes

```python
@validated_request(
request_serializer=EventCaptureRequestSerializer,
responses={
200: OpenApiResponse(response=EventCaptureResponseSerializer),
400: OpenApiResponse(response=ErrorResponseSerializer),
500: OpenApiResponse(response=ErrorResponseSerializer),
},
)
def capture_event(self, request):
try:
return Response(..., status=status.HTTP_200_OK)
except ValidationError as e:
return Response(
{"type": "validation_error", "code": "invalid", "detail": str(e)},
status=status.HTTP_400_BAD_REQUEST,
)
```

#### No response body

Use `None`:

```python
@validated_request(
responses={
204: None, # No response body
},
)
def delete_item(self, request, pk):
return Response(status=status.HTTP_204_NO_CONTENT)
```

#### Validation modes

By default, `@validated_request` uses strict validation for requests (raises on invalid data) and non-strict for responses (logs warnings in DEBUG mode). You can override:

```python
@validated_request(
request_serializer=MySerializer,
responses={200: OpenApiResponse(response=MyResponseSerializer)},
strict_request_validation=False, # Log warnings instead of raising
strict_response_validation=True, # Raise on invalid responses
)
def my_endpoint(self, request):
# ...
```

---

## Which endpoints have validated request/response definitions

The `@validated_request` decorator is new and many endpoints have not been annotated yet. The following are annotated:

- `tasks`
- `task-runs`
- `feature_flags`
- `feature_value`

The plan is to slowly annotate all endpoints with `@validated_request` through Q1 2026.

---

## The special case for `Capture`

Ingestion is essentially an entirely different service and is **not** included in the OpenAPI spec. It also has special limitations like batching and rate limiting that need to be documented separately. It doesn't fit the classic patterns for a RESTful API as well as other endpoints.

The ingestion team and docs team need to work together to update the OpenAPI spec for the `Capture` endpoint.
Loading
Loading