Skip to main content

Writing Documentation

When to Use This Skill

  • Creating a new package README
  • Writing website documentation (MDX)
  • Creating or updating skills
  • Reviewing documentation for quality
  • Documenting Phase 7 of create-destination or create-source

Prerequisites


Documentation Types

Where Content Belongs

TypePurposeAudience
Package READMEInstallation, basic usage, API referencePackage users
Website docsGuides, integration examples, detailed configIntegrators
SkillsProcess knowledge, workflowsAI assistants, contributors

Divio Documentation Types

Keep these separate - don't mix tutorials with reference:

TypePurposeUser State
TutorialLearningStudying, beginner
How-To GuideProblem-solvingWorking, knows what they need
ReferenceInformation lookupWorking, needs facts
ExplanationUnderstandingStudying, needs context

Example Validation (CRITICAL)

The Problem

AI-generated examples can be:

  • Syntactically correct but use non-existent APIs
  • Plausible-looking but don't match actual exports
  • Outdated, referencing deprecated patterns

Source of Truth Hierarchy

TIER 1: apps/quickstart/
✓ Tested ✓ Compiled ✓ CI-validated
→ USE FOR: All code examples

TIER 2: packages/core/src/eventGenerator.ts
✓ Canonical events ✓ Real data structures
→ USE FOR: Event examples

TIER 3: packages/*/src/index.ts exports
✓ Actual public API
→ USE FOR: Verifying API names exist

TIER 4: Package READMEs & Website docs
⚠ May contain errors
→ VERIFY against Tier 1-3 before trusting

Pinned onboarding examples

Getting-started and onboarding code examples are sourced from Tier-1 apps/quickstart/src/*.ts (CI-validated) first, then the docs source them. The current canonical examples:

  • first-event.ts, the zero-to-first-event console example used by the quickstart and integrated-mode pages and the README.
  • ga4-ecommerce.ts, the product add to GA4 add_to_cart mapping used by the GA4 reference page and the GA4 ecommerce tutorial.

These are pinned against drift by the doc-lint at apps/scripts/validate-docs.ts (run npx tsx apps/scripts/validate-docs.ts). It extracts canonical tokens from the Tier-1 source and asserts the docs snippets contain them, tolerating intentional doc simplifications (dropping the seen capture, arrow push instead of a method). first-event.ts is pinned on the quickstart, integrated-mode, and README pages, plus the GA4 ecommerce page; react.mdx is pinned shape-only (it has no elb('page view') push).

The doc-lint also runs these structural and coherence checks:

  • Bundled-snippet platform guard: every JSON flow with bundle carries config.platform (getting-started, README, flow reference).
  • Docker env guard: quickstart Docker snippets use BUNDLE, not FLOW.
  • Forbidden CLI verbs: no walkeros serve or standalone walkeros simulate in getting-started, guides, or the gtag page.
  • Same-page simulate-key resolution (getting-started): every push --simulate <type>.<key> must resolve to a step key declared in a flow.json on the SAME page; a simulate command on a page with no flow.json fails. This is what catches an integrated page showing a bundled push loop.
  • Flow.json schema validation (all docs): every full version: 4 flow snippet is run through schemas.validateFlowConfig from @walkeros/core/dev; fragments and version: 3 migration "before" snippets are out of scope.
  • Boundary register + :::cloud placement (getting-started + MCP page): no prices, tier names, or upgrade language; :::cloud is forbidden on golden-path pages and required where an app-only capability is documented.

The rule: when you edit a pinned example, edit the Tier-1 .ts source and re-run the doc-lint. If the docs snippet must change too, update both so the lint passes.

Getting-started editorial rules

Three rules govern the getting-started surface (decided 2026-07-06, encoded in the doc-lint):

See your event, per mode. Integrated-mode pages (quickstart, react, nextjs, modes/integrated, ga4-ecommerce) verify via their own console output; they must never show walkeros push --simulate. Only bundled-mode pages may show it, and only targeting a <type>.<key> that a flow.json on the SAME page defines.

Open/paid boundary (Self-Host Sufficiency Rule). Every getting-started task must reach a working result with MIT packages, the CLI, and reader-controlled infrastructure only. The hosted app appears at most once per page, only as a marked optional alternative, only via the :::cloud admonition, and only on the Deploy page and the MCP page. The :::cloud template: first sentence names the capability, second names the open-source path to the same result, optional third links https://app.walkeros.io. Never prices, tier names, or upgrade language anywhere in open docs. Self-host is always listed first.

Examples recipes. Pages under getting-started/examples/ are lean proof recipes with a fixed shape: exactly one flow.json, one run loop, one expected-output block, plus a short intro and next steps. The run loop uses walkeros push … --simulate <type>.<key> where <key> is a step key declared in the page's own flow.json; recipes are bundled flows, so the per-mode rule applies and the same-page simulate gate enforces resolution. The expected-output block shows what actually arrives (the captured destination call or the logged event), derived from the package's step examples or the real CLI output; never invent config, mappings, or output. Every snippet is derived from validated material (package examples, tested sources, existing validated docs snippets). No :::cloud on recipe pages. The shape is enforced by checkRecipeProof in apps/scripts/validate-docs.ts.

Validation Checklist

Before publishing ANY code example:

  • API exists? Check packages/*/src/index.ts exports
  • Pattern validated? Compare against apps/quickstart/
  • Events canonical? Use patterns from eventGenerator.ts
  • Example compiles? TypeScript check passes
  • Imports correct? Package names match actual packages

Red Flags

Red FlagWhat It Indicates
API name not in package exportsHallucinated or outdated API
Import path doesn't match package.jsonWrong package reference
Event name with underscoreWrong format (should be space)
No imports shownContext missing, harder to validate

DRY Patterns

Configuration snippet (package pages)

When to use: Every package page for destinations, sources, transformers, and stores. The <Configuration> snippet owns the ## Configuration, ## Settings, ## Mapping, and ## Examples H2 sections. One callsite per page, after ## Installation.

import data from '@walkeros/web-destination-gtag/walkerOS.json';
import Configuration from '@site/src/components/snippets/_configuration.mdx';

## Installation

<CodeSnippet
code={`npm install @walkeros/web-destination-gtag`}
language="bash"
/>

<Configuration type="destination" data={data} />

type must be one of destination, source, transformer, store. data is the full walkerOS.json import. The snippet:

  • emits ## Configuration with a short pointer to the shared group-level configuration reference,
  • emits ## Settings using <PropertyTable schema={data.schemas.settings} /> when the schema is present, otherwise a "no package-specific settings" note,
  • emits ## Mapping using <PropertyTable schema={data.schemas.mapping} /> when present, otherwise a pointer to the standard rule fields,
  • emits ## Examples only if data.examples.env.init or data.examples.step.* is present; iterates examples.step and renders each through <StepExample>.

Never hand-roll ## Configuration, ## Settings, ## Mapping, or ## Examples on a package page. Place package-specific content (vendor quirks, advanced usage, platform-specific setup) BELOW the <Configuration> block with its own sentence-case H2 heading. Fold ## Features bullet lists into the intro paragraph as prose — no ## Features heading on package pages.

Shared configuration reference (group index pages)

On group index pages (docs/destinations/index.mdx, sources/index.mdx, transformers/index.mdx, stores/index.mdx), add a ## Configuration section that documents the wrapper fields around settings — fields shared across all packages of that type (consent, mapping, env, id, …).

Drive the table from the core Zod schemas, filtering dev-only fields at the representation layer:

import { schemas } from '@walkeros/core/dev';
import { omitSchemaProperties } from '@site/src/utils/schema';

export const destinationConfig = omitSchemaProperties(
schemas.DestinationSchemas.configJsonSchema,
['settings', 'init', 'mock', 'onError', 'onLog'],
);

## Configuration

<PropertyTable schema={destinationConfig} />

Available schemas:

  • schemas.DestinationSchemas.configJsonSchema
  • schemas.SourceSchemas.configJsonSchema
  • schemas.TransformerSchemas.configJsonSchema
  • schemas.StoreSchemas.configJsonSchema

Use omitSchemaProperties to hide settings (documented per-package) and any dev-only fields (init, mock, chainMocks, onError, onLog) from the user-facing table.

Code display: CodeView vs CodeBox

@walkeros/explorer exports two components for code display:

  • <CodeBox> renders Monaco, the full editor. Use when the user can edit the code (playgrounds, interactive demos). Ships the IDE.
  • <CodeView> renders Shiki, read-only. Use for static code in docs, marketing pages, and anywhere editing is not a requirement. Same <Box> frame as <CodeBox> (traffic lights, header, copy button, tabs) so visuals match. Zero Monaco runtime cost.

Tokens match because Shiki uses the same TextMate grammars as VS Code / Monaco, with the matching dark-plus / light-plus themes.

<CodeView> and <CodeSnippet> render their code server-side, so it appears in the static HTML and the markdown/llms.txt export. Code is shown exactly as authored (no automatic reformatting), so format multi-line snippets before putting them in the code prop.

Rule: default to <CodeView>. Only reach for <CodeBox> when the code must be editable.

PropertyTable component

<PropertyTable schema={...} /> from @walkeros/explorer is the primitive. It renders a pure table — no heading, no captions. Never wrap it in a custom heading on package pages; use the <Settings /> snippet instead.

When NOT to use:

  • Pages without package configuration
  • Reference tables (Logger API, CLI commands)
  • Conceptual explanations

Schema Exports (dev.ts)

Every destination/source should export schemas:

// src/dev.ts
export * as schemas from './schemas';
export * as examples from './examples';

Don't Duplicate

  • Link to source files instead of copying type definitions
  • Reference apps/quickstart/ examples instead of writing from scratch
  • Use PropertyTable instead of hardcoded markdown tables

Writing Hints (src/hints.ts)

Hints are the "experienced colleague" layer in walkerOS.json — they tell AI agents when, why, and what to watch out for beyond what schemas and examples convey. Surfaced via MCP package_get. Not human-facing docs.

Audience: AI agents configuring packages on behalf of users.

Core Principle: Expand Awareness, Don't Narrow It

Hints should open up the space of what's possible, not prescribe a single path. An LLM reading hints should think "I have more options than I realized" — not "I must follow these steps exactly."

Writing Rules

RuleDoDon't
Describe capabilities"Supports SA key, ADC, and custom client""Use a SA key file when outside GCP"
Reference schemas/examples"See settings.projectId in the schema"Repeat what the schema description says
Explain why behind defaults"Defaults to EU location; override via location""Set location to US"
Flag non-obvious interactions"When snakeCase: true, all data keys transform before send"Describe obvious behavior
Symptoms → causes"Empty table? Check projectId and dataset existence"Step-by-step fix instructions

Key Naming

  • kebab-case, group related hints with prefixes: auth-*, storage-*, query-*, troubleshoot-*
  • Keep keys descriptive enough to scan: auth-methods not a1

When to Add Hints

Most packages don't need hints — schemas and examples cover the common case. Add hints when:

  • Multiple auth or config strategies exist and it's non-obvious when to use which
  • Non-obvious default behaviors need explaining
  • Features interact in ways the schema can't express
  • Prerequisites outside walkerOS are required
  • Common troubleshooting patterns exist

Accuracy Check

Before publishing hints, verify each claim is factually correct. Don't describe features that aren't implemented. Don't assume behavior — confirm it.

Quality Check

  • Each hint expands awareness (describes capabilities, not prescriptions)
  • Code snippets reference schema fields, not duplicate them
  • No hint restates what a schema .describe() already says
  • Troubleshooting hints use symptom → cause format
  • Hints are atomic — one concept per hint
  • Every claim is factually correct against current implementation

Export Pattern

// src/hints.ts
import type { Hints } from '@walkeros/core';

export const hints: Hints = {
'auth-methods': {
text: 'Supports three auth methods: ...',
code: [{ lang: 'json', code: '{ "settings": { ... } }' }],
},
};
// src/dev.ts
export * as schemas from './schemas';
export * as examples from './examples';
export { hints } from './hints';

Note: hints is a direct export (not * as), because it's already a Record<string, Hint>.


Quality Checklist

Structure

  • Follows appropriate Divio type (Tutorial/How-To/Reference/Explanation)
  • Code example within first 100 words
  • First example under 20 lines
  • Uses <details> for advanced content
  • Has "Next Steps" or "Related" section

Content

  • All event names use "entity action" format with space
  • Flow config shown as primary usage pattern
  • Examples are complete and copy-pasteable
  • Includes imports in code examples

AI Readability

  • Clear semantic headers (H2, H3, H4 hierarchy - no skipped levels)
  • Tables for structured data
  • Links to source of truth TypeScript files
  • Static fallback content alongside dynamic components

Consistency

  • Uses standard table formats
  • Follows package/skill/website templates
  • Terminology matches: walkerOS, collector, destination, source
  • Headings use sentence case (e.g., "Next steps" not "Next Steps")
  • walkerOS.json convention followed (walkerOS field in package.json, buildDev in tsup)

Templates

Package README Template (thin pointer)

READMEs are thin pointers to the website docs, which are the single source of truth. A README carries only: logo, title, one-line description, install, ONE current version: 4 Quick Start, and a prominent link to the package's docs page. No ## Features, no ## Configuration Reference, no inline API reference, no ## Examples block. All reference content lives on the website so it can never drift in two places.

The docs link MUST be an absolute https://www.walkeros.io/docs/... URL, never a relative path: packages publish to npm individually and relative links break on npmjs.com. These absolute docs links are validated by npm run validate:links (it maps the URL to the website/docs/ source file and asserts it exists).

<p align="left">
<a href="https://www.walkeros.io">
<img alt="walkerOS" title="walkerOS" src="https://www.walkeros.io/img/walkerOS_logo.svg" width="256px"/>
</a>
</p>

# @walkeros/[package-name]

[one-sentence description, adapted from the docs page intro]

[Documentation](https://www.walkeros.io/docs/[path]) &bull;
[NPM Package](https://www.npmjs.com/package/@walkeros/[package-name]) &bull;
[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/[path])

## Installation

```bash
npm install @walkeros/[package-name]
```

## Quick start

```json
{
"version": 4,
"flows": {
"default": {
"config": { "platform": "[web|server]" },
"[sources|destinations]": {
"[name]": {
"package": "@walkeros/[package-name]",
"config": {}
}
}
}
}
}
```

## Documentation

Full configuration, mapping, and examples live in the docs:
**https://www.walkeros.io/docs/[path]**

## Contribute

Feel free to contribute by submitting an
[issue](https://github.com/elbwalker/walkerOS/issues), starting a
[discussion](https://github.com/elbwalker/walkerOS/discussions), or getting in
[contact](https://calendly.com/elb-alexander/30min).

## License

MIT

Verify the Quick Start against apps/quickstart/ and the package's real export name before publishing. Platform-agnostic packages (core, collector) show their most representative current usage instead of a config.platform flow.

walkerOS.json

Every package should document its walkerOS.json convention in the README:

{
"walkerOS": { "type": "destination", "platform": "web" }
}

The walkerOS field is an object with type and platform metadata describing the package's role in the walkerOS ecosystem.

Website Doc Template (MDX)

Website docs fold features into the intro paragraph (no ## Features heading). Thin-pointer READMEs also have no ## Features heading; key capabilities go into the one-line description. This template is website-only.

---
title: [Title]
description: [SEO description]
sidebar_position: [N]
---

import data from '@walkeros/[package]/walkerOS.json';
import Configuration from '@site/src/components/snippets/_configuration.mdx';

# [Title]

<PackageLink package="@walkeros/[package]" />

[1-sentence description that folds in key features as prose — no separate `##
Features` heading on website docs.]

## Quick start

```json
// Flow config example (<15 lines)
```

Installation

<CodeSnippet code={npm install @walkeros/[package]} language="bash" />

<Configuration type="destination" data={data} />

Next steps

  • Related guide 1
---

## Priority Matrix

### Issue Classification

| Priority | Criteria | Action |
|----------|----------|--------|
| **P0 Critical** | Incorrect examples, wrong APIs, security issues | Fix immediately |
| **P1 High** | Missing PropertyTable, outdated domains, missing sections | Fix soon |
| **P2 Medium** | Inconsistent terminology, skipped headings | Plan to fix |
| **P3 Low** | Style issues, minor wording | Backlog |

---

## Non-Negotiables

### Punctuation

Never use em dashes (`—`). Use a comma, period, or rephrase the sentence
instead.

```text
CORRECT: "free and without sampling caps"
WRONG: "free — without sampling caps"
```

### Event Naming

Event naming (`"entity action"`, space not underscore): see
[walkeros-understanding-events](../walkeros-understanding-events/SKILL.md).

Package References


CORRECT: `@walkeros/collector` (with backticks) WRONG: @walkeros/collector (no
backticks)

Domain References


CORRECT: `www.walkeros.io` or relative paths DO NOT USE: legacy domain
references


Process

For New Package Documentation

  1. Verify examples exist in apps/quickstart/ or create them first
  2. Write README using template above
  3. Write website doc using MDX template
  4. Run quality checklist
  5. Verify all code examples against Tier 1-3 sources

For Documentation Updates

  1. Identify issue priority using matrix above
  2. Check current state against source of truth
  3. Make minimal changes - don't over-engineer
  4. Verify examples still compile
  5. Run quality checklist

Schema naming (.meta({id, title}))

Every exported schema in packages/core/src/schemas/*.ts (and in the *Schema exports of destination / source / transformer / store packages) must carry .meta({ id, title, description }) so the generated JSON Schema links back to the canonical TypeScript name. Without meta, the website's PropertyTable falls back to __schema0, object, or any.

Convention:

  • id — PascalCase, namespace-prefixed so it is globally unique across every schema in packages/core/src/schemas. Examples: WalkerOSConsent, DestinationConfig, CollectorPushContext, LoggerConfig.
  • title — dotted form matching the VS Code TS hover: WalkerOS.Consent, Destination.Config, Collector.PushContext, Logger.Config.
  • description — one short sentence. May repeat an existing .describe() where both are present.

Reference implementation (copy this shape for new schemas):

export const ConsentSchema = z.record(z.string(), z.boolean()).meta({
id: 'WalkerOSConsent',
title: 'WalkerOS.Consent',
description: 'Consent state mapping (group name to granted state).',
});

Coverage is enforced by packages/core/src/schemas/__tests__/meta-coverage.test.ts. When adding a new schema, run that test — it will fail with the exact schema name if meta is missing.

Destination / source / transformer / store packages that ship their own SettingsSchema should also call .meta({ id, title }) on it, e.g. DestinationGtag.Settings / GtagDestinationSettings.


💡 Need implementation support?
elbwalker offers hands-on support: setup review, measurement planning, destination mapping, and live troubleshooting. Book a 2-hour session (€399)