From walkeros
Guides creation of walkerOS destinations via example-driven workflow: research vendor SDKs/APIs, classify taxonomy, define in/out examples, map events, scaffold from templates (plausible/gtag/gcp), implement, test, document.
npx claudepluginhub elbwalker/walkerosThis skill uses the workspace's default tool permissions.
Before starting, read these skills:
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
Before starting, read these skills:
Flow.StepExample structure and Three Type Zones| Complexity | Template | When to Use |
|---|---|---|
| Simple | plausible/ | Single SDK call, minimal config |
| Complex | gtag/ | Multiple services, sub-destinations |
| Server | gcp/ | Server-side, batching, SDK init |
1. Research → Deeply understand vendor SDK, API, and event taxonomy
2. Classify → Determine vendor taxonomy type and integration approach
3. Examples → Define in/out pairs FIRST (start with the end result)
4. Mapping → Define walkerOS → vendor transformation
5. Scaffold → Copy template and configure
6. Convention → Add walkerOS.json metadata and buildDev
7. Implement → Build using examples as test fixtures
8. Test → Verify against example variations
9. Document → Write README
Goal: Deeply understand the vendor SDK before writing any code. Research quality determines implementation quality.
Always prefer the vendor's official SDK package over raw HTTP API calls. The SDK handles transport, batching, retries, and plugin ecosystems — don't reinvent these.
npm install @vendor/sdk and read the actual sourcetrack(). Identity methods, property operations, group management,
specialized event types (revenue, etc.)# Search npm for official packages
npm search [vendor-name]
npm search @[vendor]
# Install and inspect actual types
npm install @vendor/analytics-browser
ls node_modules/@vendor/analytics-browser/lib/esm/
init() accept? What can be configured?init()? Is there
internal queuing? What are the race condition implications?Go beyond just track(). Most SDKs have specialized methods:
| Method Category | Examples | walkerOS Handling |
|---|---|---|
| Event tracking | track(), logEvent() | Default push() |
| User properties | identify(), setUserProperties() | mapping.settings |
| Revenue/purchase | revenue(), purchase() | mapping.settings |
| Groups/accounts | setGroup(), groupIdentify() | mapping.settings |
| Identity setters | setUserId(), setDeviceId() | settings.identify |
| Opt-out | setOptOut(), consent() | on('consent') |
| Cleanup | flush(), reset() | destroy() |
Review similar destinations in the codebase:
ls packages/web/destinations/
If working with human oversight, pause here to confirm:
Goal: Understand what the vendor expects, which determines destination complexity.
| Type | Description | Mapping Needed | Example Vendors |
|---|---|---|---|
| Free-form | Any event name accepted, no prescribed schema | Minimal — names pass through | Most modern analytics |
| Rigid | Prescribed event names unlock specific reports | Essential — must map to exact names | Some legacy analytics |
| Schema-based | Self-describing events with formal schemas | Structural — must build schema objects | Data warehouse tools |
| Approach | When to use | Pattern |
|---|---|---|
| Vendor SDK as host | SDK has plugins, batching, retries | Load SDK, disable what walkerOS replaces, use as transport |
| Script + command queue | SDK loaded via script tag | Load script, use global function queue |
| HTTP API | No SDK available, or server-side | Direct HTTP calls via sendWeb() or fetch |
Prefer the vendor SDK — it handles transport, retries, and plugin orchestration. HTTP API is a fallback when no SDK exists.
When using the vendor SDK:
Mandatory. Examples are the test fixtures for Phase 8. Define expected in
/ out / mapping triples FIRST — start with the end result in mind. Without
examples, you cannot test. Even for free-form vendors where no mapping is
strictly "required," step examples serve as the single source of truth for
tests, simulations, and documentation.
Authoritative pattern: See using-step-examples for the Three Type Zones and lifecycle. This skill reuses that contract — do not diverge.
mkdir -p packages/web/destinations/[name]/src/examples
mkdir -p packages/web/destinations/[name]/src/{schemas,types}
All seven reference web destinations (gtag, meta, snowplow, plausible, piwikpro,
api, demo) use exactly three files in src/examples/. Match this structure — no
events.ts, outputs.ts, or standalone mapping.ts.
| File | Purpose |
|---|---|
examples/env.ts | Mock environment for testing (no real network calls) |
examples/step.ts | Flow.StepExample entries with in / out / mapping? triples |
examples/index.ts | Barrel exports: env and step |
The step.ts file embeds the input event, the mapping config, and the expected
vendor output together in one Flow.StepExample — subsuming what older skills
described as separate events.ts / outputs.ts / mapping.ts files.
No any. Every example value must be explicitly typed.
WalkerOS.Event (via getEvent() from @walkeros/core) —
never hand-roll event literals.fbq argument types,
not a local FbqCall interface).Flow.StepExample from @walkeros/core.Env type from
../types. No as any, no untyped {}.examples/step.tsimport type { Flow, WalkerOS } from '@walkeros/core';
import { getEvent } from '@walkeros/core';
// One step example per supported feature / setting.
// `in` is a WalkerOS.Event (use getEvent for deterministic fixtures).
// `out` is the vendor-specific call we expect the destination to produce —
// typed against the vendor SDK's published types where available.
// `mapping` is the mapping rule under test (optional — omit for default push).
// Set `title` + `description` for public examples; mark test-only fixtures
// with `public: false`. See
// [walkeros-using-step-examples](../walkeros-using-step-examples/SKILL.md).
export const purchase: Flow.StepExample = {
in: getEvent('order complete', { timestamp: 1700000100 }),
mapping: {
name: 'purchase',
data: {
map: {
transaction_id: 'data.id',
value: 'data.total',
currency: { key: 'data.currency', value: 'EUR' },
},
},
},
out: [
'event',
'purchase',
{
transaction_id: '0rd3r1d',
value: 555,
currency: 'EUR',
},
],
};
export const pageView: Flow.StepExample = {
in: getEvent('page view', { timestamp: 1700000102 }),
mapping: undefined,
out: ['event', 'page_view', { send_to: 'G-XXXXXX-1' }],
};
// For destinations that handle consent updates, use the command field
// to route `in` through elb('walker consent', in) instead of an event push:
export const consentGranted: Flow.StepExample = {
command: 'consent',
in: { marketing: true, functional: true },
out: ['consent', 'update', { ad_storage: 'granted' }],
};
Every destination ships an examples.step.init entry — the init is a
first-class step example, not a hidden side effect.
in mirrors the real Destination.Config shape users copy-paste — typically
{ loadScript: true, settings: { /* vendor-specific */ } }. Whatever a user
would configure in their flow goes here verbatim.out is the ordered list of vendor calls the init() lifecycle produces
(script tags, SDK initializers, queue setup). Each effect tuple follows the
standard [callable, ...args] shape.Test pattern. Call
destination.init({ id, config, env, logger, collector }) directly in the test
— no capture helpers, no capture.ts, no allowlists. Assert the captured vendor
calls equal examples.step.init.out:
const calls: unknown[][] = [];
const env = wrapEnv(examples.env.init, (call) => calls.push(call));
await destination.init({
id: 'test',
config: examples.step.init.in as Destination.Config,
env,
logger,
collector,
});
expect(calls).toEqual(examples.step.init.out);
Event step tests bootstrap once with examples.step.init.in, then slice the
shared capture buffer to isolate push effects from init effects:
const pushCalls = calls.slice(examples.step.init.out.length);
expect(pushCalls).toEqual(example.out);
There are no hand-maintained allowlists or isInitEffect filters — the init
example's out.length is the single source of truth for how many effects belong
to init.
Multi-tool packages (like gtag, which drives GA4, Google Ads, and GTM)
ship per-tool init examples — examples.step.ga4Init, adsInit, gtmInit
— instead of a single init. The docs render each on its own page via
<StepExample example={data.examples.step.ga4Init} />, and tests pick the right
init per sub-tool.
For destinations, the Three Type Zones collapse to:
in = walkerOS event (WalkerOS.Event)out = vendor output (typed against vendor SDK)mapping = rule under test (optional)examples/index.ts (barrel)export * as env from './env';
export * as step from './step';
examples/env.tsMock the vendor SDK surface and any DOM touchpoints. Never reach real network,
real cookies, or real globals. Type the exports against your local Env:
import type { Env } from '../types';
export const init: Env | undefined = {
/* pre-init state (vendor SDK not yet loaded) */
};
export const push: Env = {
/* post-init state used for push() tests */
};
The examples authored here are the Phase 8 test fixtures. No parallel fixtures allowed.
src/index.test.ts (or src/__tests__/stepExamples.test.ts) MUST iterate
examples via it.each(Object.entries(examples.step)).examples.step, add it to step.ts
first, then consume it from the test. Never inline test data.settings from the
example's mapping.settings (e.g. enabling the right sub-tool).See packages/web/destinations/gtag/src/__tests__/stepExamples.test.ts for a
canonical reference.
dev.tsexport * as schemas from './schemas';
export * as examples from './examples';
src/examples/env.ts — mock env, no real network, typed against local
Envsrc/examples/step.ts — one Flow.StepExample per supported feature /
setting, typed in / out / mapping?src/examples/index.ts — barrel exports env and stepevents.ts, outputs.ts, or mapping.ts filesany, no
reinvented local output typessrc/index.test.ts (or __tests__/stepExamples.test.ts) iterates
examples.step via it.each(Object.entries(...))examples.stepnpm run build passes — examples compile against published typesin → apply mapping → matches outGoal: Document transformation from walkerOS events to vendor format.
Mapping rules live inside each Flow.StepExample entry in step.ts — no
separate mapping.ts file. Each step example embeds the exact mapping rule
under test alongside its in event and expected out output.
For each entry in step.ts, trace:
Input: examples.step.purchase.in (WalkerOS.Event)
↓ Apply examples.step.purchase.mapping
↓ name transforms, data.map applied
Output: Should match examples.step.purchase.out
mapping traces correctly from in to outTemplate destination: packages/web/destinations/plausible/
cp -r packages/web/destinations/plausible packages/web/destinations/[name]
cd packages/web/destinations/[name]
# Update package.json: name, description, repository.directory
Directory structure:
packages/web/destinations/[name]/
├── src/
│ ├── index.ts # Main destination (init + push)
│ ├── index.test.ts # Tests against examples
│ ├── dev.ts # Exports schemas and examples
│ ├── examples/
│ ├── schemas/
│ └── types/
├── package.json
├── tsconfig.json
├── tsup.config.ts
├── jest.config.mjs
└── README.md
Destinations can wire to transformer chains via before in the init config:
destinations: {
myDestination: {
code: destinationMyDestination,
config: { settings: { /* ... */ } },
before: 'redact' // Events go through redactor before this destination
}
}
Every walkerOS package ships a walkerOS.json file for CDN-based schema
discovery.
walkerOS field to package.json{
"walkerOS": { "type": "destination", "platform": "web" },
"keywords": ["walkerOS", "walkerOS-destination", ...]
}
buildDev() in tsup.config.tsReplace buildModules({ entry: ['src/dev.ts'] }) with buildDev():
import { buildDev } from '@walkeros/config/tsup';
// In defineConfig array:
buildDev(),
This auto-generates dist/walkerOS.json from your Zod schemas at build time.
If your destination has capabilities, behaviors, or troubleshooting patterns not
obvious from schemas alone, add hints. See walkeros-writing-documentation
skill for full guidelines.
Create src/hints.ts:
import type { Hint } from '@walkeros/core';
export const hints: Hint.Hints = {
'auth-methods': {
text: 'Supports X, Y, and Z auth methods. See settings schema for all options.',
code: [{ lang: 'json', code: '{ "settings": { ... } }' }],
},
};
Export from src/dev.ts:
export * as schemas from './schemas';
export * as examples from './examples';
export { hints } from './hints';
Guidelines:
walkerOS field in package.json with type and platformbuildDev() in tsup.config.tsdist/walkerOS.jsonwalkerOS and walkerOS-destinationNow write code to produce the outputs defined in Phase 3.
Use these templates as your starting point:
| File | Purpose | Template |
|---|---|---|
types/index.ts | Type definitions | types.ts |
index.ts | Main destination | index.ts |
config, env, logger, id from
contextdata, rule (renamed from mapping),
ingestgetEnv(env): Never access window/document directlydestroy method: Implement if the destination holds resources
(DB connections, SDK clients, timers) that need cleanup on shutdown. Call
flush() or equivalent on the vendor SDK.map, loop, key, value, condition) in mapping.settings.*
for vendor-specific operations. Resolve via getMappingValue() in push()
and interpret the resolved object's keys as SDK method instructions. This
keeps config agnostic and reuses the mapping engine.config.consent gates walkerOS event delivery.
on('consent') controls vendor SDK internals (opt-out, pause capture, etc.).
Both needed for complete consent compliance.
command: 'consent' on Flow.StepExample to
invoke the on('consent') handler. Do not push consent data as an event.npm run build passesnpm run lint passesTests verify implementation against the examples from Phase 3. If examples are incomplete, tests will be incomplete.
See testing-strategy for the shared env / dev-examples conventions this phase depends on.
Verify implementation produces expected outputs.
Use the test template: index.test.ts.
Reference canonical implementation:
packages/web/destinations/gtag/src/__tests__/stepExamples.test.ts.
it.each(Object.entries(examples.step)) is mandatory — one iteration per
step example. Do not write per-feature tests with hand-rolled payloads.createPushContext() helper — standardizes context creation.id field — required in context.rule instead of mapping — property renamed in PushContext.examples.step or examples.env. If you need something new, add
it to examples first.clone(examples.env.push) so vendor mocks don't
leak across iterations.npm run test passesit.each(Object.entries(examples.step))examples.step[...].outFollow the writing-documentation skill for:
apps/quickstart/Key requirements for destination documentation:
Beyond
understanding-development
requirements (build, test, lint, no any):
getEnv(env) pattern (never direct window/document access)dev.ts exports schemas and exampleswalkerOS.json generated at build timewalkerOS field in package.json| What | Where |
|---|---|
| Simple template | packages/web/destinations/plausible/ |
| Complex example | packages/web/destinations/gtag/ |
| Types | packages/web/core/src/types/destination.ts |
Flow.StepExample pattern and Three Type Zones