name: walkeros-using-step-examples description: Use when writing, simulating, validating, or testing with walkerOS step examples. Covers the complete lifecycle from authoring examples to CI integration.
Using Step Examples
Overview
Step examples are structured { in, out } pairs (with optional mapping for
destinations) that define the expected input/output behavior of each step in a
flow. They serve as:
- Test fixtures for automated
it.eachtesting - Simulation data for
walkeros push --simulate - MCP context for AI-assisted development
- Documentation showing real-world usage
Every source, transformer, and destination can ship step examples alongside its code.
The Three Type Zones
Each step in a flow sits at a boundary between arbitrary external formats and the walkerOS event model:
Source Transformer Destination
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ in: arbitrary │ │ in: walkerOS │ │ in: walkerOS │
│ (HTTP req, DOM) │ │ Event │ │ Event │
│ │ │ │ │ │ │ │ │
│ ▼ │ │ ▼ │ │ ▼ │
│ out: walkerOS │ │ out: walkerOS │ │ out: arbitrary │
│ Event │ │ Event | false │ │ (gtag, API) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
arbitrary ──────────── walkerOS.Event ──────────── arbitrary
- Source converts platform-specific input into a walkerOS event
- Transformer receives and returns walkerOS events (or
falseto filter) - Destination converts a walkerOS event into vendor-specific output
The in/out/trigger Format
Every step example is an object with in (input), out (array of observable
effects), and optional trigger (how to invoke):
type StepEffect = readonly [callable: string, ...args: unknown[]];
type StepOut = readonly StepEffect[];
interface StepExample {
in: unknown; // Platform-specific input
out: StepOut; // Array of effect tuples - see shape rules below
trigger?: { type?: string; options?: unknown }; // How to invoke
mapping?: unknown; // Destination mapping rule
command?: 'config' | 'consent' | 'user' | 'run'; // Route to walker command
}
out is always an array of effect tuples, even for a single effect. Each
tuple is [callable, ...args]. The first element is the callable's public name
(the SDK function users would write). Remaining elements are the arguments.
Callable conventions
| Component type | Callable | Notes |
|---|---|---|
| Destination (SDK function) | 'gtag', 'fbq', 'ttq.track' |
Public SDK name. Dotted paths render literally. |
| Destination (method on global) | 'analytics.track', 'dataLayer.push' |
Method notation. |
| Destination (HTTP) | 'fetch', 'sendServer' |
The actual call users would make. NOT env.* prop names - those are internal plumbing. |
| Source | 'elb' |
The walker public push API. |
| Transformer | 'return' |
Reserved keyword. Renders as return <value> (no parens). |
'return' is reserved. Don't use it for anything else.
Empty out: [] means the step produced no observable effect (filtered input,
transformer passthrough, validator rejection). Reserved only for cases where the
destination/source deliberately emits nothing.
Source Step Example - Server (Express)
in is an HTTP request shape, out is a tuple of the elb() call the source
makes:
export const checkoutPost: Flow.StepExample = {
trigger: { type: 'POST' },
in: {
method: 'POST',
path: '/collect',
body: { name: 'order complete', data: { id: 'ORD-123', total: 149.97 } },
},
out: [
['elb', { name: 'order complete', data: { id: 'ORD-123', total: 149.97 } }],
],
};
Source Step Example - Browser
in is an HTML string, out is a tuple of the elb() call:
export const clickEvent: Flow.StepExample = {
trigger: { type: 'click', options: 'button' },
in: '<button data-elb="cta" data-elb-cta="label:Sign Up" data-elbaction="click:click">Sign Up</button>',
out: [
[
'elb',
{
name: 'cta click',
data: { label: 'Sign Up' },
trigger: 'click',
entity: 'cta',
action: 'click',
},
],
],
};
Transformer Step Example
in is a walkerOS event, out is a ['return', value] tuple. An empty array
means the transformer passed the event through unchanged:
export const step = {
orderPasses: {
in: { name: 'order complete', data: { id: 'ORD-123' } },
out: [['return', { name: 'order complete', data: { id: 'ORD-123' } }]],
},
debugFiltered: {
in: { name: 'debug test', data: { message: 'noise' } },
out: [['return', false]], // Transformer rejects
},
passthrough: {
in: { name: 'page view' },
out: [], // No modification
},
};
Destination Step Example
in is a walkerOS event, mapping is the mapping rule that transforms it,
out is an array of call tuples - one per observable effect the destination
produces. Multi-call events (e.g., GA4 + Ads + GTM for a single walker event)
flatten into a single array in execution order:
import type { Flow } from '@walkeros/core';
import { getEvent } from '@walkeros/core';
export const purchase: Flow.StepExample = {
in: getEvent('order complete', { timestamp: 1700000000 }),
mapping: {
name: 'Purchase',
data: { map: { value: 'data.total', currency: { value: 'EUR' } } },
},
out: [
[
'fbq',
'track',
'Purchase',
{ value: 555, currency: 'EUR' },
{ eventID: '1700000000-gr0up-1' },
],
],
};
Multi-tool example (one walker event produces GA4, Ads, GTM in order):
out: [
['gtag', 'event', 'purchase', { transaction_id: 'o1', value: 555 }],
['gtag', 'event', 'conversion', { send_to: 'AW-123', value: 555 }],
['dataLayer.push', { event: 'purchase', ecommerce: { ... } }],
],
Each export is a self-contained Flow.StepExample - no intermediate variables,
no all aggregation. The mapping field ties the mapping rule to the example
so tests can register it dynamically:
{ [event.entity]: { [event.action]: example.mapping } }.
Consumers iterate all examples via Object.entries(examples.step) -
export * as step exposes every named export directly.
Command Step Example
Some destinations need to respond to walker commands (walker consent,
walker user, walker run), not events. Set the command field to route in
through elb('walker <command>', in) instead of pushing it as an event:
export const consentGranted: Flow.StepExample = {
command: 'consent',
in: { marketing: true, functional: true },
out: [
'consent',
'update',
{ ad_storage: 'granted', analytics_storage: 'granted' },
],
};
Supported commands: config, consent, user, run.
- When
commandis absent (default),inis pushed as a regular event viaelb(in). - When
commandis set,mappingis not applied - commands don't flow through event mapping. - The
outformat is destination-specific. For gtag it's[action, subAction, params]matchinggtag(...)calls.
Writing Examples
Best Practices
- Use camelCase names -- descriptive of the scenario (
checkoutPost,debugFiltered,pageViewBasic) - Cover happy path and edge cases -- include at least one positive and one negative case for transformers
- Use realistic data -- real-looking event names, IDs, and values
- Keep examples minimal -- only include fields relevant to the step's
logic; omit empty/default-value fields (
context: {},nested: [],user: {}, etc.) - Export from
examples/step.ts-- follow the existingdev.tsstructure - Title and describe public examples -- add
title(2-5 words) anddescription(one short sentence, 10-25 words) for every example that should appear in docs. Mark test-only fixtures withpublic: false.
File Structure
// src/examples/step.ts - only Flow.StepExample exports, nothing else
import type { Flow } from '@walkeros/core';
import { getEvent } from '@walkeros/core';
export const purchase: Flow.StepExample = {
in: getEvent('order complete', { timestamp: 1700000000 }),
mapping: { name: 'Purchase', data: { map: { value: 'data.total' } } },
out: ['track', 'Purchase', { value: 555 }, { eventID: '1700000000-gr0up-1' }],
};
// src/examples/index.ts
export * as env from './env';
export * as step from './step';
The file exports only Flow.StepExample objects. No intermediate variables,
no all, no config. Consumers iterate via Object.entries(examples.step).
Rule: examples live only on the ./dev subpath
Examples (and schemas) must be exported only from src/dev.ts (the ./dev
package subpath), never from a production entry (src/index.ts / the package
main). Production bundles tree-shake on the import path: if examples is
reachable from the main entry, the bundler inlines every example (and its
getEvent fixtures) into the shipped JS, bloating consumer bundles with
test-only data. Importing them from ./dev keeps them available for simulation
and testing while leaving production output clean.
A no-restricted-syntax ESLint rule enforces this, so a export ... examples
from a production entry fails lint.
Metadata: title, description, public
Every Flow.StepExample accepts three optional metadata fields that control how
it surfaces in docs and MCP output:
title?: string- overrides the default camelCase-to-spaced heading in website docs. Keep it short (2-5 words), human-readable.description?: string- one short sentence (10-25 words) rendered above each example in docs and surfaced in MCPflow_examplesoutput. Explains what the example demonstrates, not how.public?: boolean- defaults totrue. Whenfalse, the example is excluded from the website docs render and from default MCPflow_examplesoutput. It still runs in tests and remains callable via CLI/MCPflow_simulate.
export const purchase: Flow.StepExample = {
title: 'Purchase',
description:
'A completed order mapped to the Meta Pixel Purchase standard event.',
in: getEvent('order complete', { timestamp: 1700000000 }),
mapping: { name: 'Purchase', data: { map: { value: 'data.total' } } },
out: [['fbq', 'track', 'Purchase', { value: 555, currency: 'EUR' }]],
};
// Internal fixture - runs in tests, hidden from docs and default MCP output.
export const debugFiltered: Flow.StepExample = {
public: false,
in: { name: 'debug test', data: { message: 'noise' } },
out: [['return', false]],
};
When to mark public: false
Mark public: false when:
- The example is a transformer rejection (
out: [['return', false]]) that only proves filtering works. - It's an error-path or malformed-input case.
- It's a redundant variant kept only for regression coverage.
- It's an internal fixture used by cross-package tests.
Keep public (omit the field) when:
- It's the happy path.
- It teaches a distinct mapping or usage pattern.
- It's the only example for some entity/action the package handles.
Source Trigger Metadata
Source step examples can include a trigger field for simulation:
{
in: '<button data-elb="cta">Sign Up</button>',
trigger: { type: 'click' },
out: { name: 'cta click', data: { label: 'Sign Up' } }
}
When simulating via CLI or MCP, the step example maps to SourceInput:
in->content(the actual source input)trigger->trigger(which mechanism to fire)
Destination and transformer examples don't use trigger.
Simulating with Step Examples
Use the --step flag to target a specific step, then provide the event as
SourceInput ({ content, trigger? }):
# Simulate a source step with trigger metadata
walkeros push flow.json --simulate source.browser --event '{"content":"<html>...","trigger":{"type":"click"}}'
The MCP flow_examples tool returns trigger metadata alongside in/out,
and mapping for destination examples, giving full visibility into how input
events are transformed to vendor-specific output.
Validating Examples
Cross-step example validation is included automatically when validating a flow:
# Validate flow config including step example compatibility
walkeros validate flow.json
Flow validation checks that:
- Source
outtypes match transformerintypes - Transformer
outtypes match destinationintypes - Connected steps have compatible examples
Testing with Examples
The primary use of step examples is automated testing with it.each.
Source Tests (createTrigger)
Sources export createTrigger from their examples. It follows the unified
Trigger.CreateFn interface: receives Collector.InitConfig, lazily starts the
flow, and returns a trigger function that simulates real-world invocations.
Each package implements createTrigger differently:
- Browser: Injects HTML into DOM, starts flow, dispatches native events
- Express: Boots real HTTP server, sends
fetch()requests
Use a spy destination to capture events:
import type { Destination, WalkerOS } from '@walkeros/core';
import { sourceExpress } from '@walkeros/server-source-express';
import { examples } from '@walkeros/server-source-express/dev';
describe('Step Examples', () => {
it.each(Object.entries(examples.step))('%s', async (name, example) => {
const events: WalkerOS.Event[] = [];
const spy: Destination.Instance = {
type: 'spy',
config: { init: true },
push: jest.fn((event) => {
events.push(JSON.parse(JSON.stringify(event)));
}),
};
const instance = await examples.createTrigger({
consent: { functional: true },
sources: {
express: { code: sourceExpress, config: { settings: { port: 0 } } },
},
destinations: { spy: { code: spy } },
});
await instance.trigger(example.trigger?.type)(example.in);
const found = events.find((e) => e.name === example.out.name);
expect(found).toBeDefined();
});
});
Browser source note: The browser source's elbLayer processes events via a detached promise chain (fire-and-forget). For interactive triggers (click, submit), poll until events arrive:
while (!events.length) await Promise.resolve();
CMP Source Tests (Consent Assertion)
CMP sources push walker consent commands, not regular events. Assert on
collector consent state instead of using a spy destination:
const instance = await examples.createTrigger({
consent: {},
sources: {
usercentrics: {
code: sourceUsercentrics,
config: { settings: {} },
},
},
});
await instance.trigger(
example.trigger?.type,
example.trigger?.options,
)(example.in);
// Yield for detached elb('walker consent') chain
while (!Object.keys(instance.flow!.collector.config.consent || {}).length)
await Promise.resolve();
expect(instance.flow!.collector.config.consent).toEqual(
expect.objectContaining(expected),
);
Server Function Handler Tests (collector.sources)
Server function handlers (fetch, AWS Lambda, GCP CloudFunction) don't own
servers. Their createTrigger accesses the source instance from
collector.sources after startFlow and calls source.push() with
platform-native types:
function findSource(collector) {
for (const source of Object.values(collector.sources || {})) {
if (source.type === 'fetch') return source;
}
}
// In trigger:
const source = findSource(flow.collector);
const response = await source.push(request);
Destination Functional Tests
For destinations, use startFlow + elb() to run events through the real
collector pipeline. This verifies the full flow including mapping:
import { startFlow } from '@walkeros/collector';
import { clone } from '@walkeros/core';
import { examples } from './dev';
describe('Step Examples', () => {
it.each(Object.entries(examples.step))('%s', async (name, example) => {
const event = example.in as WalkerOS.Event;
const mapping = example.mapping as Rule | undefined;
const mockFn = jest.fn();
const env = clone(examples.env.push);
env.window.fbq = mockFn;
const dest = jest.requireActual('.').default;
const { elb } = await startFlow({});
// Build mapping config from event entity/action
const mappingConfig = mapping
? { [event.entity]: { [event.action]: mapping } }
: undefined;
elb('walker destination', {
code: { ...dest, env },
config: {
settings: { pixelId: '1234567890' },
mapping: mappingConfig,
},
});
await elb(event);
expect(mockFn).toHaveBeenLastCalledWith(...(example.out as unknown[]));
});
});
Transformer Tests
For transformers that can return false:
import { examples } from '../dev';
describe('transformer', () => {
it.each(Object.entries(examples.step))(
'%s',
async (name, { in: input, out: expected }) => {
const result = await transformer.push(input, context);
if (expected === false) {
expect(result).toBe(false);
} else {
expect(result).toEqual(expected);
}
},
);
});
Checklist
When adding step examples to a package or flow:
- Create
src/examples/step.tswithFlow.StepExampleexports - Export from
src/examples/index.ts - Use camelCase, descriptive example names
- Include at least one happy-path example
- For transformers, include a
false(filtered) case - Add
it.eachtest using step examples - Verify examples compile:
npm run build - Run tests:
npm run verify:touched -- <pkg>(L1) - Add
titleanddescriptionto every public example. - Mark internal/test-only examples with
public: false.
Related Skills
- testing-strategy - Testing patterns and env mocking
- understanding-flow - Architecture and data flow
- using-cli - CLI simulate and validate commands
- create-destination - Destination creation workflow
- create-source - Source creation workflow
- create-transformer - Transformer creation workflow