@walkeros/cli 4.7.0-next-1790245733116 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,255 +1,914 @@
1
- # flow-complete.json - Comprehensive walkerOS Flow Example
1
+ # flow-complete.json: the guide
2
+
3
+ One real ecommerce setup in one file, taught in 15 chapters. Every feature below
4
+ points into [`flow-complete.json`](./flow-complete.json) with an RFC 6901 JSON
5
+ Pointer, and every feature id comes from the manifest (`@walkeros/cli/examples`,
6
+ `flowCompleteManifest`), so the guide, the docs and the talk name the same
7
+ things.
8
+
9
+ - **Scenario:** a shop with `page view`, `product view`, `product impression`,
10
+ `product add`, `order complete` and `session start`.
11
+ - **Three flows:** `web` in the browser, `server` behind it, `warehouse` reading
12
+ the Pub/Sub topic into BigQuery.
13
+ - **How to read a chapter:** purpose, features (id, pointer, one line), the
14
+ command lines, the MCP tools, how GTM does it, the doc pages.
15
+ - **Where to run the commands:** the walkerOS repo root, where `npx walkeros`
16
+ resolves the workspace CLI. Elsewhere install `@walkeros/cli` (binary
17
+ `walkeros`) and `@walkeros/runner` (binary `runneros`). Lines below are
18
+ written without `npx`.
19
+ - **Environment for simulation:** the CLI reads the shell environment, not a
20
+ `.env` file. Run this once in the repo root before the server lines: `GCP_SA`
21
+ holds a service account JSON (a throwaway key is enough for simulation: every
22
+ Google client runs on its mock), `FINGERPRINT_SALT`, `EMAIL_SALT` and
23
+ `META_ACCESS_TOKEN` have no defaults on purpose, and `CUSTOMERS_DIR` points
24
+ the customers store at the demo data. Every other value is `$env.NAME:default`
25
+ and works out of the box.
2
26
 
3
- This example demonstrates **ALL JSON-compatible walkerOS features** with
4
- real-world patterns. It contains two named flows (`web` and `server`) showing a
5
- complete event tracking architecture.
27
+ ```bash
28
+ export CUSTOMERS_DIR="$PWD/packages/cli/examples/customers"
29
+ export FINGERPRINT_SALT=demo-fingerprint-salt EMAIL_SALT=demo-email-salt META_ACCESS_TOKEN=demo-meta-token
30
+ export GCP_SA="$(node -e "const { generateKeyPairSync } = require('crypto'); const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, publicKeyEncoding: { type: 'spki', format: 'pem' } }); console.log(JSON.stringify({ type: 'service_account', project_id: 'demo-project', client_email: 'demo@demo-project.iam.gserviceaccount.com', private_key: privateKey }))")"
31
+ ```
32
+
33
+ - **There is no `walkeros simulate` and no `walkeros run`:** simulate with
34
+ `walkeros push ... --simulate <kind>.<name>`, run with `walkeros bundle` then
35
+ `runneros start`.
6
36
 
7
- ## Architecture
37
+ ```text
38
+ web usercentrics, session, dataLayer, browser
39
+ -> collector.next: pageGroup
40
+ -> ga4 (to Google, main property) gtm (dataLayer) collect (to server)
8
41
 
42
+ server express /collect /g/collect /walker.js
43
+ before: one [ G-SUBSITE hit -> ga4Decode -> ga4Consent | *.js -> file (stop) | other /g/collect (stop) ]
44
+ next: dedup
45
+ -> collector.next: fingerprint, bot, not impression -> enrich [ loadUser, sessionSave, sessionLoad, validate ]
46
+ -> pubsub (every event, email pseudonymized)
47
+ -> meta, piwikpro, datamanager (each behind eventFilter)
48
+
49
+ warehouse pubsub pull -> bigquery (keeps everything)
9
50
  ```
10
- ┌─────────────────────────────────────────────────────────────────────────────┐
11
- │ WEB FLOW │
12
- │ │
13
- │ Browser Source ─┐ │
14
- │ DataLayer Source ──▶ [Enricher] ──▶ Collector ──▶ GA4 │
15
- │ Demo Source ────┘ ├──▶ API │
16
- │ └──▶ Debug │
17
- └─────────────────────────────────────────────────────────────────────────────┘
18
51
 
19
- ┌─────────────────────────────────────────────────────────────────────────────┐
20
- │ SERVER FLOW │
21
- │ │
22
- │ HTTP Source ──▶ [Filter] ──▶ [Fingerprint] ──▶ Collector │
23
- │ │ │ │ │
24
- │ │ ingest: │ env: ▼ │
25
- │ │ IP, UA, │ filter rules Meta Destination │
26
- │ │ lang, ref, │ Demo Destination │
27
- │ │ anon-IP(fn) │ │
28
- │ └──────────────┘ │
29
- │ │
30
- │ Source cache: built-in tier (ttl 300s, on GET ingest.method+path) │
31
- └─────────────────────────────────────────────────────────────────────────────┘
52
+ ## Part I: see it work
53
+
54
+ ## tour: One file, one event
55
+
56
+ - **Purpose:** see the whole file pass the strict check, then watch one event
57
+ come out of a source.
58
+ - **Features:**
59
+ - `schema-url` at `/$schema`: The JSON Schema gives editors and agents
60
+ completion and checks for every field.
61
+ - `format-version` at `/version`: version 4 is the flow file format this file
62
+ is written in.
63
+ - `flows-multi` at `/flows`: One file holds three flows: web in the browser,
64
+ server behind it, warehouse reading the topic.
65
+ - `platform` at `/flows/web/config/platform`: platform decides how a flow is
66
+ built: a browser bundle or a server artifact.
67
+ - `flow-config` at `/flows/web/config`: config holds what the build needs:
68
+ platform, url, settings, bundle, observe.
69
+ - `flow-sources` at `/flows/web/sources`: Sources turn whatever happens (a CMP
70
+ decision, a dataLayer push, a click) into events.
71
+ - `flow-transformers` at `/flows/server/transformers`: Transformers are named
72
+ hops that validate, enrich, filter or remember events.
73
+ - `flow-destinations` at `/flows/server/destinations`: Destinations fan out:
74
+ every event reaches each destination that accepts it.
75
+ - `flow-collector` at `/flows/server/collector`: The collector sits between
76
+ sources and destinations and runs its own chain once per event.
77
+ - **CLI:** strict validation checks the schema, every reference, every package
78
+ version, every step's settings and every example link; with the published
79
+ packages of this release this file gives 0 errors and 0 warnings. Three steps
80
+ import a named export, which has no settings schema yet, so they are listed as
81
+ skipped and `--strict` exits 2 on them.
82
+
83
+ ```bash
84
+ walkeros validate packages/cli/examples/flow-complete.json --strict
85
+ walkeros push packages/cli/examples/flow-complete.json -f web -e '{"name":"page view"}' --simulate source.browser
32
86
  ```
33
87
 
34
- ## Quick Start
88
+ - The second line starts the browser source in a simulated page and prints the
89
+ page view it captures (`trigger: "load"`, `source.type: "browser"`).
90
+ - **MCP:** `flow_validate`, `flow_simulate`, `flow_load`.
91
+ - **In GTM:** preview mode on one container; here one file holds the browser,
92
+ the server and the warehouse, and all three are checked together.
93
+ - **Docs:** `getting-started/flow/index`, `collector/index`.
94
+
95
+ ## web-entry: Start where the shop already is
96
+
97
+ - **Purpose:** capture events from what the page already has: the Usercentrics
98
+ banner, the dataLayer the shop pushes to, data-elb tagging.
99
+ - **Features:**
100
+ - `cmp-usercentrics` at `/flows/web/sources/usercentrics`: The Usercentrics
101
+ source turns the banner decision into a walker consent command.
102
+ - `session-source` at `/flows/web/sources/session`: The session source sets
103
+ user.session and user.device and emits session start with the raw click id.
104
+ - `datalayer-source` at `/flows/web/sources/dataLayer`: The dataLayer source
105
+ reads the pushes the shop already makes, so nothing on the page changes.
106
+ - `source-require` at `/flows/web/sources/dataLayer/config/require`: require
107
+ holds the source until consent and session are known.
108
+ - `source-mapping` at `/flows/web/sources/dataLayer/config/mapping`: Source
109
+ mapping renames dataLayer add_to_cart to product add and picks its fields.
110
+ - `browser-source` at `/flows/web/sources/browser`: The browser source reads
111
+ data-elb tagging: page views, clicks and impressions.
112
+ - `source-primary` at `/flows/web/sources/browser/primary`: primary names the
113
+ source whose instance the flow exposes, here the browser elb.
114
+ - `browser-pageview` at `/flows/web/sources/browser/config/settings/pageview`:
115
+ pageview: true sends a page view on load without tagging.
116
+ - `collector-globals` at `/flows/web/collector/globals`: globals.language is
117
+ the default the page can override; the contract requires it.
118
+ - `collector-globals-static` at `/flows/web/collector/globalsStatic`:
119
+ globalsStatic survives a run reset; the site id belongs there.
120
+ - `collector-custom` at `/flows/web/collector/custom`: custom carries values
121
+ for the pipeline that are not event data.
122
+ - `config-url` at `/flows/web/config/url`: url says where a flow lives; other
123
+ flows reach it with $flow.
124
+ - `config-settings` at `/flows/web/config/settings`: Platform settings:
125
+ windowCollector names the global collector instance.
126
+ - `require: ["consent", "session"]` on the dataLayer source means nothing is
127
+ read before the banner decision and the session are known.
128
+ - The browser source is `primary`: its `elb` is the one the page calls.
129
+ - Usercentrics and session examples are shown in the browser (their simulation
130
+ in jsdom is still waiting), the dataLayer and browser examples simulate.
131
+ - **CLI:** the tour line above (`--simulate source.browser`); the same form
132
+ works for `source.dataLayer`.
133
+ - **MCP:** `package_get` (source schemas and examples), `flow_simulate`.
134
+ - **In GTM:** a consent initialization trigger plus a CMP template, and the
135
+ container reading the same dataLayer.
136
+ - **Docs:** `guides/session`, `collector/index`.
137
+
138
+ ## server-entry: A first-party endpoint, and a GA4 migration path
139
+
140
+ - **Purpose:** one express source answers the browser (`/collect`), GA4 hits of
141
+ a sub-site (`/g/collect`) and script requests (`/walker.js`).
142
+ - **Features:**
143
+ - `server-url` at `/flows/server/config/url`: The full collect endpoint, local
144
+ by default, SERVER_URL in production.
145
+ - `express-paths` at `/flows/server/sources/express/config/settings/paths`:
146
+ One route per job, each with its own methods: /collect, /g/collect,
147
+ /walker.js.
148
+ - `express-port` at `/flows/server/sources/express/config/settings/port`: port
149
+ lets push --simulate start the source; runneros mounts the handler on its
150
+ own port.
151
+ - `express-cors` at `/flows/server/sources/express/config/settings/cors`: CORS
152
+ response headers for the shop origin; express adds X-Content-Type-Options
153
+ itself.
154
+ - `source-ingest` at `/flows/server/sources/express/config/ingest`: ingest
155
+ keeps the request context (ip, path, headers) for later steps without
156
+ putting it on the event.
157
+ - `ingest-headers` at
158
+ `/flows/server/sources/express/config/ingest/map/userAgent`: Request headers
159
+ are read by path: headers.user-agent, headers.accept-language.
160
+ - `source-async` at `/flows/server/sources/express/config/async`: POST answers
161
+ at once; GET waits so a step can answer with real content.
162
+ - `ga4-decode` at `/flows/server/transformers/ga4Decode`: GA4 hits from a
163
+ sub-site not yet migrated (its own property) become events; the main site
164
+ never sends here and hits of any other property are not decoded, so nothing
165
+ counts twice.
166
+ - **CORS and request headers:** `settings.cors` sets the CORS response headers
167
+ for the shop origin; `config.ingest` reads request headers by path
168
+ (`headers.user-agent`, `headers.accept-language`) into `ingest`, where later
169
+ steps read them without putting them on the event.
170
+ - **GA4 migration path, no double counting:**
171
+ - GA4 hits from a sub-site not yet migrated (its own property) become events;
172
+ the main site never sends here and hits of any other property are not
173
+ decoded, so nothing counts twice.
174
+ - The sub-site property is `G-SUBSITE`: its gtag transport points at our
175
+ server, and the route decodes only hits whose `tid` is `G-SUBSITE`.
176
+ - The main-site property is `G-MAINSITE`: the web `ga4` destination sends it
177
+ straight to Google, never to this server.
178
+ - Sub-site traffic arrives only via `/g/collect`, main-site traffic only via
179
+ `/collect`.
180
+ - walkerOS has no server GA4 destination, so decoded hits are forwarded to
181
+ other tools, never back to GA4.
182
+ - Decoded hits carry the browser language (`ul`, e.g. `en-us`) in
183
+ `user.language`; `ga4Consent` derives `globals.language` from it (`en`, or
184
+ `na` when the hit has none), so a decoded hit with analytics consent passes
185
+ the contract and reaches the vendors like any other event.
186
+ - **CLI:** build the server artifact, then run it. `GCP_SA` must be set (Pub/Sub
187
+ and Data Manager read it as credentials); no step calls Google at start, so a
188
+ throwaway key is enough to become ready and serve `walker.js`, while pushes to
189
+ Pub/Sub and Data Manager need a real one. The artifact carries `shared/` but
190
+ not the demo customers: set `CUSTOMERS_DIR` to the absolute path of
191
+ `packages/cli/examples/customers` in `.env`. The env file must be private
192
+ (`chmod 600 .env`), `runneros` refuses a group-readable one.
35
193
 
36
- ### Run Web Flow
194
+ ```bash
195
+ walkeros bundle packages/cli/examples/flow-complete.json -f server -o dist/server.mjs
196
+ chmod 600 .env
197
+ runneros start dist/server.mjs -p 8080 --env-file .env
198
+ ```
199
+
200
+ - One GA4 hit from the sub-site and the script (the event reaches Pub/Sub only
201
+ with a real key; the same requests run in the flow's integration test over
202
+ HTTP):
37
203
 
38
204
  ```bash
39
- cd walkerOS
40
- npx walkeros run packages/cli/examples/flow-complete.json --flow web
41
- # Open http://localhost:3000 - demo events fire automatically
205
+ curl -X POST 'http://localhost:8080/g/collect?v=2&tid=G-SUBSITE&cid=555.1&en=page_view&dl=https%3A%2F%2Fsub.example.com%2F'
206
+ curl -I http://localhost:8080/walker.js
42
207
  ```
43
208
 
44
- ### Run Server Flow
209
+ - **MCP:** `flow_bundle`, `package_get` (`@walkeros/transformer-ga4`,
210
+ `@walkeros/server-source-express`).
211
+ - **In GTM:** a server container client for GA4.
212
+ - **Docs:** `guides/reference-syntax` (ingest paths).
213
+
214
+ ## step-envelope: One step shape
215
+
216
+ - **Purpose:** every source, transformer, destination and store has the same
217
+ envelope, and its `examples` are the contract of the step.
218
+ - **Features:**
219
+ - `step-package` at `/flows/server/destinations/meta/package`: package names
220
+ the npm package that implements a step.
221
+ - `step-import` at `/flows/server/destinations/pubsub/import`: import picks a
222
+ named export when one package ships several steps.
223
+ - `step-code` at `/flows/web/transformers/pageGroup/code`: code defines a step
224
+ inline, no package needed.
225
+ - `code-push` at `/flows/web/transformers/pageGroup/code/push`: push is the
226
+ step function, written as $code.
227
+ - `code-type` at `/flows/web/transformers/pageGroup/code/type`: type names the
228
+ inline step in logs and Observe.
229
+ - `step-config` at `/flows/server/destinations/meta/config`: config is the
230
+ same envelope for every step kind: settings, mapping, consent, reliability.
231
+ - `step-settings` at `/flows/server/destinations/meta/config/settings`:
232
+ settings are the package options, checked against the package schema.
233
+ - `step-env` at `/flows/server/transformers/file/env`: env injects what a step
234
+ uses at runtime, here the assets store.
235
+ - `step-examples` at `/flows/web/destinations/ga4/examples`: examples are the
236
+ contract of a step: this in gives this out, checked by simulation.
237
+ - `example-title` at `/flows/web/destinations/ga4/examples/purchase/title`:
238
+ title names the example.
239
+ - `example-description` at
240
+ `/flows/web/destinations/ga4/examples/purchase/description`: description is
241
+ the one teaching sentence the docs render.
242
+ - `example-public` at `/flows/web/destinations/ga4/examples/purchase/public`:
243
+ public marks the one example per step the docs show first.
244
+ - `example-in` at `/flows/web/destinations/ga4/examples/purchase/in`: in is
245
+ what the step receives.
246
+ - `example-out` at `/flows/web/destinations/ga4/examples/purchase/out`: out is
247
+ what the step does, as effects: gtag calls, a return, a request.
248
+ - `example-mapping` at
249
+ `/flows/web/destinations/ga4/examples/purchase/mapping`: mapping shows the
250
+ rule the example exercises.
251
+ - `example-trigger` at
252
+ `/flows/web/sources/browser/examples/productImpression/trigger`: trigger
253
+ says how a source example fires: load, click, impression.
254
+ - `example-command` at
255
+ `/flows/web/destinations/ga4/examples/consentUpdate/command`: command routes
256
+ in through a walker command instead of an event, here consent.
257
+ - **CLI:** check one step against its package schema, then run a destination
258
+ example by hand.
45
259
 
46
260
  ```bash
47
- cd walkerOS
48
- npx walkeros run packages/cli/examples/flow-complete.json --flow server
261
+ walkeros validate packages/cli/examples/flow-complete.json -f web --path destinations.collect
262
+ walkeros push packages/cli/examples/flow-complete.json -f web -e '{"name":"order complete","data":{"id":"ORD-100","total":149.8,"currency":"EUR"}}' --simulate destination.ga4
263
+ ```
49
264
 
50
- # Test health
51
- curl http://localhost:8080/health
265
+ - The second line prints `mapping: none (skipped before mapping)`: the event has
266
+ no functional consent, so GA4 never sees it (see `consent-privacy`).
267
+ - Any example `in` is a ready fixture:
268
+ `jq .flows.server.destinations.meta.examples.purchase.in packages/cli/examples/flow-complete.json > events/order-complete.json`.
269
+ - **MCP:** `flow_examples`, `flow_simulate`, `package_get`.
270
+ - **In GTM:** a tag template with its fields; here the same envelope carries
271
+ settings, mapping, consent and reliability for every step kind.
272
+ - **Docs:** `getting-started/flow/step-examples`.
273
+
274
+ ## Part II: shape the data
275
+
276
+ ## mapping: Vendor mapping
277
+
278
+ - **Purpose:** turn walkerOS events into vendor calls, one rule per entity and
279
+ action, with values read, computed, looped and defaulted (London prompt 2).
280
+ - **Features:**
281
+ - `ga4-purchase` at
282
+ `/flows/web/destinations/ga4/config/mapping/order/complete`: order complete
283
+ becomes a GA4 purchase with value, currency and items.
284
+ - `destination-mapping` at `/flows/web/destinations/ga4/config/mapping`:
285
+ mapping holds one rule per entity and action.
286
+ - `rule-name` at
287
+ `/flows/web/destinations/ga4/config/mapping/order/complete/name`: name is
288
+ the vendor event name.
289
+ - `rule-data` at
290
+ `/flows/web/destinations/ga4/config/mapping/order/complete/data`: data
291
+ builds the vendor parameters from the event.
292
+ - `value-map` at
293
+ `/flows/web/destinations/ga4/config/mapping/order/complete/data/map`: map
294
+ builds an object key by key.
295
+ - `value-key` at
296
+ `/flows/web/destinations/ga4/config/mapping/order/complete/data/map/coupon/key`:
297
+ key reads a path from the event.
298
+ - `value-fn` at
299
+ `/flows/web/destinations/ga4/config/mapping/order/complete/data/map/value/fn`:
300
+ fn computes a value from the event, here the total rounded to cents. fn and
301
+ key are alternatives: next to a key, fn never runs.
302
+ - `value-loop` at
303
+ `/flows/web/destinations/ga4/config/mapping/order/complete/data/map/items/loop`:
304
+ loop turns nested products into GA4 items.
305
+ - `value-condition` at
306
+ `/flows/web/destinations/ga4/config/mapping/order/complete/data/map/coupon/condition`:
307
+ A value condition sends coupon only when the order has one.
308
+ - `value-fallback` at
309
+ `/flows/web/destinations/ga4/config/mapping/order/complete/data/map/currency`:
310
+ An array of values is a fallback list: the event currency, else the flow
311
+ default.
312
+ - `value-set` at
313
+ `/flows/web/destinations/ga4/config/mapping/product/view/data/map/items/set`:
314
+ set builds an array, here the one item of a product view.
315
+ - `value-value` at
316
+ `/flows/server/destinations/meta/config/mapping/product/add/data/map/currency/value`:
317
+ value is a constant.
318
+ - `rule-condition` at
319
+ `/flows/web/destinations/ga4/config/mapping/order/complete/condition`: A
320
+ rule condition skips the rule, here for orders without a total.
321
+ - `rule-settings` at
322
+ `/flows/server/destinations/piwikpro/config/mapping/order/complete/settings`:
323
+ Rule settings are per-event package options: the Piwik PRO goal for orders.
324
+ - `mapping-wildcard-ignore` at
325
+ `/flows/server/destinations/meta/config/mapping/*/*/ignore`: A wildcard rule
326
+ with ignore sends Meta only what is mapped.
327
+ - `destination-include` at `/flows/web/destinations/ga4/config/include`:
328
+ include flattens whole sections into parameters, here the globals.
329
+ - `rule-extend` at
330
+ `/flows/server/transformers/ga4Decode/config/settings/mapping/purchase/extend`:
331
+ extend patches a package default rule instead of replacing it.
332
+ - `rule-remove` at
333
+ `/flows/server/transformers/ga4Decode/config/settings/mapping/purchase/remove`:
334
+ remove drops fields from the default rule output.
335
+ - **CLI:** the GA4 purchase, with consent and one product:
52
336
 
53
- # Send test event
54
- curl -X POST http://localhost:8080/collect \
55
- -H "Content-Type: application/json" \
56
- -d '{
57
- "name": "order complete",
58
- "data": {"id": "ORD-999", "total": 99.99, "currency": "EUR"},
59
- "user": {"email": "test@example.com", "id": "U-123"},
60
- "nested": [{"entity": "product", "data": {"id": "P1", "name": "Item", "price": 99.99, "quantity": 1}}]
61
- }'
337
+ ```bash
338
+ walkeros push packages/cli/examples/flow-complete.json -f web -e '{"name":"order complete","data":{"id":"ORD-100","total":149.8,"currency":"EUR"},"nested":[{"entity":"product","data":{"id":"SKU-1","name":"Linen shirt","price":74.9,"quantity":2}}],"consent":{"functional":true}}' --simulate destination.ga4
62
339
  ```
63
340
 
64
- ### Run Both (Full Pipeline)
341
+ - Output:
342
+ `call window.gtag("event","purchase",{... "transaction_id":"ORD-100","value":149.8,"currency":"EUR","items":[{"item_id":"SKU-1",...}],"send_to":"G-MAINSITE"})`,
343
+ with the globals flattened in by `include`.
344
+ - **MCP:** `package_get` (mapping hints), `flow_simulate`.
345
+ - **In GTM:** a GA4 event tag with an ecommerce variable per field.
346
+ - **Docs:** `getting-started/flow/step-examples`.
347
+
348
+ ## consent-privacy: Consent and personal data
349
+
350
+ - **Purpose:** consent at the destination, the rule and the field; `policy`
351
+ before mapping; what happens to the email and the IP address.
352
+ - **Features:**
353
+ - `cmp-category-map` at
354
+ `/flows/web/sources/usercentrics/config/settings/categoryMap`: categoryMap
355
+ maps CMP categories onto exactly the consent keys the contract names.
356
+ - `cmp-explicit-only` at
357
+ `/flows/web/sources/usercentrics/config/settings/explicitOnly`: explicitOnly
358
+ publishes consent only after a real decision, so consent.functional present
359
+ means the user decided.
360
+ - `session-consent` at `/flows/web/sources/session/config/settings/consent`:
361
+ Storage for session ids waits for the functional consent key.
362
+ - `ga4-consent-map` at `/flows/server/transformers/ga4Consent`: A mapping hop
363
+ after the decoder copies GA4 analytics storage into functional and derives
364
+ the page language from the browser language (na when the hit has none);
365
+ refused pings stay functional false and fail the contract.
366
+ - `destination-consent` at `/flows/server/destinations/meta/config/consent`:
367
+ Destination consent: Meta only gets events with marketing consent.
368
+ - `rule-consent` at
369
+ `/flows/web/destinations/ga4/config/mapping/order/complete/consent`: Rule
370
+ consent on purchases: functional, like the destination; marketing consent
371
+ belongs to Meta and Data Manager.
372
+ - `value-consent` at
373
+ `/flows/web/destinations/collect/config/policy/user.email/consent`: Field
374
+ consent: the email is only kept with marketing consent.
375
+ - `destination-policy` at `/flows/web/destinations/ga4/config/policy`: policy
376
+ rewrites the event before mapping, here a currency fallback.
377
+ - `rule-policy` at
378
+ `/flows/server/destinations/meta/config/mapping/order/complete/policy`: Rule
379
+ policy sets the event id to the order id, so Meta can match it with the
380
+ browser pixel.
381
+ - `meta-hashing` at
382
+ `/flows/server/destinations/meta/config/settings/user_data/em`: Meta
383
+ normalizes and hashes em itself; the flow never handles a hash.
384
+ - `value-validate` at
385
+ `/flows/server/destinations/meta/config/settings/user_data/em/validate`:
386
+ validate drops a value that does not look like an email.
387
+ - `ingest-ip-raw` at `/flows/server/sources/express/config/ingest/map/ip`: The
388
+ raw ip stays in ingest: Meta needs it, fingerprint anonymizes it itself.
389
+ - `email-pseudonymize` at `/flows/server/transformers/pseudonymize`: Before
390
+ Pub/Sub the email becomes a stable keyed hash (HMAC-SHA256, EMAIL_SALT);
391
+ Meta and Data Manager keep the raw email and hash it themselves.
392
+ - `pubsub-before` at `/flows/server/destinations/pubsub/before`: A conditional
393
+ next on Pub/Sub runs pseudonymize only when an email exists; other events
394
+ pass untouched.
395
+ - `ga4-como` at `/flows/web/destinations/ga4/config/settings/como`: como turns
396
+ walker consent into Google Consent Mode calls.
397
+ - **Consent keys:** there are no collector consent defaults, only granted keys
398
+ travel. `consent.functional` is set only by Usercentrics with `explicitOnly`,
399
+ so its presence means the user decided.
400
+ - **Meta hashes `user_data` itself:** `em` is the raw email; the Meta package
401
+ normalizes and hashes it before sending, the flow never builds a hash for
402
+ Meta.
403
+ - **Email in the warehouse:** before Pub/Sub, the `pseudonymize` step
404
+ (`@walkeros/server-transformer-fingerprint`, HMAC-SHA256 with `EMAIL_SALT`, no
405
+ rotation) replaces `user.email` with a keyed hash, so the warehouse never
406
+ stores a raw email; Meta and Data Manager receive the raw email and hash it
407
+ themselves.
408
+ - **CLI:** the step-envelope line shows the consent skip; this one shows Meta
409
+ and Piwik PRO with request context from `--ingest`:
65
410
 
66
411
  ```bash
67
- # Terminal 1: Start server
68
- npx walkeros run packages/cli/examples/flow-complete.json --flow server
412
+ walkeros push packages/cli/examples/flow-complete.json -f server -e events/order-complete.json --ingest '{"ip":"203.0.113.7","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/128.0 Safari/537.36"}' --simulate destination.meta --simulate destination.piwikpro
413
+ ```
414
+
415
+ - Output:
416
+ `call sendServer("https://graph.facebook.com/v22.0/1234567890/events", ... "event_id":"ORD-100" ... "client_ip_address":"203.0.113.7" ...)`
417
+ and the Piwik PRO hit; credentials never appear in the output.
418
+ - **MCP:** `flow_simulate`, `package_get` (`@walkeros/server-destination-meta`).
419
+ - **In GTM:** consent settings per tag plus Consent Mode.
420
+ - **Docs:** `guides/session`.
421
+
422
+ ## gtm-fed: GTM stays, fed by walkerOS
423
+
424
+ - **Purpose:** keep the GTM container and feed it walkerOS events, without the
425
+ dataLayer source reading them back.
426
+ - **Features:**
427
+ - `gtm-mode` at `/flows/web/destinations/gtm/config/settings/gtm`: GTM stays:
428
+ the gtag destination in GTM mode pushes walkerOS events into its dataLayer.
429
+ - `destination-data` at `/flows/web/destinations/gtm/config/data`: Destination
430
+ data sends consent, globals, user and data with every GTM push.
431
+ - `gtm-names` at
432
+ `/flows/web/destinations/gtm/config/mapping/product/add/name`: GTM-bound
433
+ names start with elb, so the dataLayer source can skip them.
434
+ - `datalayer-filter` at `/flows/web/sources/dataLayer/config/settings/filter`:
435
+ The filter skips gtag arguments pushes and elb names: without it the page
436
+ loops.
437
+ - `gtm-echo-stop` at `/flows/web/destinations/gtm/before`: An event read from
438
+ the dataLayer never goes back to GTM.
439
+ - `load-script` at `/flows/web/destinations/ga4/config/loadScript`: loadScript
440
+ lets the destination load gtag.js itself.
441
+ - **Loop guard, two parts:** the dataLayer `filter` skips gtag `arguments`
442
+ pushes and every name starting with `elb `; the `gtm` destination's `before`
443
+ stops events whose `source.type` is `dataLayer`, so GTM never gets back what
444
+ it pushed.
445
+ - The `gtm` examples are shown in the browser (their simulation needs consent
446
+ seeding and is still waiting).
447
+ - **CLI:** none of its own; `walkeros validate ... --strict` checks the
448
+ destination schema.
449
+ - **MCP:** `package_get` (`@walkeros/web-destination-gtag`).
450
+ - **In GTM:** triggers on `elb product add` and `elb order complete`, variables
451
+ reading `consent`, `globals`, `user` and `data`.
452
+ - **Docs:** `getting-started/flow/routing`.
453
+
454
+ ## references: Values from outside the file
455
+
456
+ - **Purpose:** every id, token, topic and dataset comes from a reference;
457
+ resolution order step over flow over root.
458
+ - **Features:**
459
+ - `root-variables` at `/variables`: Root variables are shared by all flows; a
460
+ flow or a step can override them.
461
+ - `flow-variables` at `/flows/server/variables`: Flow variables win over root
462
+ variables for one flow.
463
+ - `step-variables` at `/flows/server/destinations/piwikpro/variables`: Step
464
+ variables win over flow and root variables for one step.
465
+ - `ref-var-whole` at
466
+ `/flows/web/destinations/ga4/config/policy/data.currency/1/value`: A
467
+ whole-string $var keeps the variable type.
468
+ - `ref-var-deep` at
469
+ `/flows/server/destinations/pubsub/config/settings/projectId`:
470
+ $var.gcp.project reads a path inside a variable.
471
+ - `ref-var-inline` at
472
+ `/flows/warehouse/sources/pubsub/config/setup/deadLetterPolicy/deadLetterTopic`:
473
+ Inside a longer string $var is replaced in place, here in a full topic name.
474
+ - `ref-env` at `/flows/server/transformers/fingerprint/config/settings/salt`:
475
+ $env without a default must be set: a secret salt never gets a public
476
+ default.
477
+ - `ref-env-default` at `/variables/gcp/project`: $env.NAME:default works out
478
+ of the box and takes the environment value when set.
479
+ - `ref-secret` at `/flows/server/destinations/pubsub/config/credentials`:
480
+ $secret is read at start and never baked into an artifact; web flows reject
481
+ it.
482
+ - `ref-flow` at `/flows/web/destinations/collect/config/settings/url`:
483
+ $flow.server.url points the browser at the server flow; it is whole-string
484
+ only.
485
+ - `ref-code` at `/flows/web/destinations/collect/config/settings/transform`:
486
+ $code: puts a function into JSON.
487
+ - `ref-contract` at
488
+ `/flows/server/transformers/validate/config/settings/contract/0`:
489
+ $contract.server hands the resolved contract to the validate step.
490
+ - `ref-store` at `/flows/server/transformers/file/env/store`: $store.assets
491
+ injects a store into a step.
492
+ - `bundle-env` at `/flows/web/config/bundle/env`: Web $env is resolved at
493
+ build time from this declared set, so every build is the same.
494
+ - **Kinds:** `$var` (whole, deep, inline), `$env.NAME` and `$env.NAME:default`,
495
+ `$secret`, `$flow`, `$code:`, `$contract`, `$store`.
496
+ - **Secrets:** `$secret` is used only for `GCP_SA`, on the server and in the
497
+ warehouse; web flows reject `$secret`.
498
+ - **CLI:** `runneros` reads a private env file:
69
499
 
70
- # Terminal 2: Start web (sends to server via API destination)
71
- npx walkeros run packages/cli/examples/flow-complete.json --flow web
500
+ ```bash
501
+ runneros start dist/server.mjs -p 8080 --env-file .env
72
502
  ```
73
503
 
74
- ---
504
+ - **MCP:** `flow_validate` (unresolved inline references are named).
505
+ - **In GTM:** constant and lookup table variables, per workspace.
506
+ - **Docs:** `guides/reference-syntax`.
507
+
508
+ ## Part III: build the pipeline
509
+
510
+ ## chains-routing: Chains and routes
511
+
512
+ - **Purpose:** decide per event which steps run: `source.before` on the raw
513
+ request, `source.next`, `collector.next` once per event, `destination.before`
514
+ per destination (London prompt 3).
515
+ - **Features:**
516
+ - `web-collector-next` at `/flows/web/collector/next`: On the web
517
+ collector.next runs the inline pageGroup step once per event.
518
+ - `ga4-other-dropped` at `/flows/server/sources/express/before/one/2`: GA4
519
+ hits for any other property stop at the route instead of reaching the
520
+ collector undecoded.
521
+ - `source-before` at `/flows/server/sources/express/before`: source.before
522
+ runs on the raw request, before an event exists.
523
+ - `route-one` at `/flows/server/sources/express/before/one`: one takes the
524
+ first matching branch; no match falls through.
525
+ - `match-and` at `/flows/server/sources/express/before/one/0/match/and`: and
526
+ needs every condition.
527
+ - `op-prefix` at
528
+ `/flows/server/sources/express/before/one/0/match/and/0/operator`: prefix
529
+ matches the start of a value.
530
+ - `op-regex` at
531
+ `/flows/server/sources/express/before/one/0/match/and/1/operator`: regex
532
+ matches a pattern: only hits of the sub-site property are decoded.
533
+ - `op-suffix` at `/flows/server/sources/express/before/one/1/match/operator`:
534
+ suffix matches the end of a value: script requests end in .js.
535
+ - `source-next` at `/flows/server/sources/express/next`: source.next names the
536
+ hop after the source, here dedup.
537
+ - `collector-next` at `/flows/server/collector/next`: collector.next runs once
538
+ per event before the fan-out: fingerprint and bot for all, enrich for all
539
+ but impressions.
540
+ - `route-next-match` at `/flows/server/collector/next/2`: A conditional next
541
+ (match plus next) runs the target only when the match holds; otherwise the
542
+ chain goes on.
543
+ - `match-not` at `/flows/server/collector/next/2/match/not`: not inverts a
544
+ condition: everything that is not an impression.
545
+ - `code-free-hop` at `/flows/server/transformers/enrich`: A step with only
546
+ next is a named chain other routes can call.
547
+ - `route-sequence` at `/flows/server/transformers/enrich/next`: A list mixing
548
+ conditional next entries and step ids is a sequence: each match decides in
549
+ turn, validate always runs last.
550
+ - `transformer-next` at `/flows/server/transformers/eventFilter/next`: A
551
+ transformer next continues the chain after the step.
552
+ - `op-exists` at `/flows/server/transformers/enrich/next/0/match/operator`:
553
+ exists checks that a path has a value: only logged-in events load the
554
+ customer.
555
+ - `op-eq` at `/flows/server/transformers/enrich/next/1/match/operator`: eq
556
+ compares as strings.
557
+ - `destination-before` at `/flows/server/destinations/meta/before`:
558
+ destination.before runs for one destination only; a stop there skips just
559
+ that one.
560
+ - `event-filter` at `/flows/server/transformers/eventFilter`: eventFilter
561
+ keeps impressions, likely bots and invalid events from Meta, Piwik PRO and
562
+ Data Manager; Pub/Sub keeps everything.
563
+ - `route-stop` at `/flows/server/transformers/eventFilter/next/stop`: stop
564
+ with a match drops the event at that point.
565
+ - `match-or` at `/flows/server/transformers/eventFilter/next/match/or`: or
566
+ needs any condition.
567
+ - `event-filter-impression` at
568
+ `/flows/server/transformers/eventFilter/next/match/or/0`: Impressions stay
569
+ out of Meta and Piwik PRO but reach the warehouse.
570
+ - `op-gt` at
571
+ `/flows/server/transformers/eventFilter/next/match/or/1/operator`: gt
572
+ compares numbers.
573
+ - `stop-unconditional` at `/flows/server/transformers/file/next`: An
574
+ unconditional stop: a script request never becomes an event.
575
+ - **Words:** a conditional next is `{ match, next }`; `one` takes the first
576
+ matching branch; `stop` ends the chain (with a `match`, only when it holds).
577
+ - **Not used here, with a pointer:** `many`, `destination.next` and
578
+ `transformer.before` have no genuine use in this scenario; see `routeCases` in
579
+ `@walkeros/core/dev` (`core/src/examples/route-cases.ts`).
580
+ - **CLI:** the collector chain for an impression (fingerprint and bot run,
581
+ `enrich` does not), and `eventFilter` alone (no events come out):
75
582
 
76
- ## Feature Inventory
77
-
78
- ### Features Used (68)
79
-
80
- #### Mapping - Value Extraction
81
-
82
- | Feature | Location | Example |
83
- | ----------------- | ----------------- | ------------------------------------------------------------ |
84
- | Key extraction | GA4 page_view | `"page_title": "data.title"` |
85
- | Static value | Meta ViewContent | `"content_type": { "value": "product" }` |
86
- | Key with fallback | GA4 add_to_cart | `{ "key": "data.currency", "value": "$variables.currency" }` |
87
- | Nested key (deep) | dataLayer mapping | `"items.0.item_id"` |
88
- | Fallback array | GA4 view_item | `[{ "key": "data.sku" }, { "key": "data.id" }]` |
89
-
90
- #### Mapping - Structure
583
+ ```bash
584
+ walkeros push packages/cli/examples/flow-complete.json -f server -e '{"name":"product impression","trigger":"impression","data":{"id":"SKU-1"}}' --simulate collector.default
585
+ walkeros push packages/cli/examples/flow-complete.json -f server -e '{"name":"product impression","trigger":"impression","data":{"id":"SKU-1"}}' --simulate transformer.eventFilter
586
+ ```
91
587
 
92
- | Feature | Location | Example |
93
- | --------------------- | ------------------ | ----------------------------------------------------------- |
94
- | Map (object) | GA4 purchase | `"data": { "map": { ... } }` |
95
- | Loop with "nested" | GA4 purchase items | `{ "loop": ["nested", { "map": {...} }] }` |
96
- | Loop with "this" | GA4 add_to_cart | `{ "loop": ["this", { "map": {...} }] }` |
97
- | Set (single value) | Meta ViewContent | `"content_ids": { "set": ["data.id"] }` |
98
- | Set (multiple values) | Meta settings | `"external_id": { "set": ["user.device", "user.session"] }` |
99
- | Direct passthrough | Meta PageView | `"data": "data"` |
100
- | Config-level data | API destination | `"data": { "map": { "sent_at": {...} } }` |
588
+ - **MCP:** `flow_simulate`, `flow_validate`.
589
+ - **In GTM:** trigger conditions and exceptions per tag.
590
+ - **Docs:** `getting-started/flow/routing`.
591
+
592
+ ## contract: The contract and the validate step
593
+
594
+ - **Purpose:** write down what every event must carry, check it at runtime, and
595
+ keep invalid events from the vendors (London prompt 4).
596
+ - **Features:**
597
+ - `contract-default` at `/contract/default`: The contract names what every
598
+ event must carry; the validate step and validate --strict read it.
599
+ - `contract-extend` at `/contract/server/extend`: server extends default with
600
+ what the server adds: the visitor hash.
601
+ - `contract-server-hash` at
602
+ `/contract/server/schema/properties/user/properties/hash`: The description
603
+ tells analysts what user.hash is and how to count it.
604
+ - `contract-tagging` at `/contract/default/tagging`: tagging is the contract
605
+ version the tagging follows.
606
+ - `contract-description` at `/contract/default/description`: description says
607
+ what the contract is for.
608
+ - `contract-schema` at `/contract/default/schema`: schema applies to every
609
+ event: functional consent is a hard gate, language a required global.
610
+ - `contract-events` at `/contract/default/events`: events hold one JSON Schema
611
+ per entity and action.
612
+ - `contract-wildcard` at `/contract/default/events/product/*`: product \*
613
+ applies to every product action.
614
+ - `validate-transformer` at `/flows/server/transformers/validate`: validate
615
+ checks each event against $contract.server at runtime.
616
+ - `validate-pass` at
617
+ `/flows/server/transformers/validate/config/settings/mode`: mode pass marks
618
+ source.valid instead of dropping, so the warehouse keeps violations; strict
619
+ is the hard gate.
620
+ - `event-filter-valid` at
621
+ `/flows/server/transformers/eventFilter/next/match/or/2`: Only events that
622
+ match the contract reach the vendors.
623
+ - **Gate semantics:** `validate` runs in `mode: "pass"`: it marks `source.valid`
624
+ and never drops, so the warehouse keeps violations. The `eventFilter` clause
625
+ `event.source.valid eq false` stops invalid events before Meta, Piwik PRO and
626
+ Data Manager only. `mode: "strict"` is the hard-gate variant: it drops invalid
627
+ events for every destination, the warehouse included.
628
+ - **CLI:** `validate` on the flow already checks the contract (every `extend`
629
+ resolved, paths under `contract.`); `-t contract` on the flow file runs that
630
+ contract check alone. Then run the validate step on an order without a visitor
631
+ hash (out: `source.valid: false`):
101
632
 
102
- #### Mapping - Control
103
-
104
- | Feature | Location | Example |
105
- | -------------------- | ------------------- | ----------------------------------------------------------- |
106
- | Consent-gated field | API destination | `{ "key": "user.email", "consent": { "marketing": true } }` |
107
- | Ignore rule | GA4/API test events | `{ "ignore": true }` |
108
- | Wildcard action (\*) | GA4 test, Meta | `"test": { "*": { "ignore": true } }` |
109
- | Wildcard entity (\*) | Meta click handler | `"*": { "click": { "name": "CustomEvent" } }` |
110
-
111
- #### Variables
112
-
113
- | Feature | Location | Example |
114
- | -------------------- | ---------------- | ---------------------------------------------- |
115
- | Root-level variables | Root | `"currency": "EUR"` |
116
- | Flow-level variables | server.variables | `"metaPixelId": "$env.META_PIXEL_ID:..."` |
117
- | Environment variable | Variables | `"$env.GA4_MEASUREMENT_ID:G-DEMO123456"` |
118
- | Env with default | Variables | `"$env.API_URL:http://localhost:8080/collect"` |
119
- | $var reference | GA4 settings | `"$var.ga4MeasurementId"` |
633
+ ```bash
634
+ walkeros validate packages/cli/examples/flow-complete.json -t contract
635
+ walkeros push packages/cli/examples/flow-complete.json -f server -e '{"name":"order complete","data":{"id":"ORD-100","total":149.8,"currency":"EUR"},"globals":{"language":"en"},"consent":{"functional":true}}' --simulate transformer.validate
636
+ ```
120
637
 
121
- #### Sources
638
+ - **MCP:** `flow_validate`, `flow_simulate`.
639
+ - **In GTM:** no equivalent; a custom template per tag at best.
640
+ - **Docs:** `getting-started/flow/contract`, `transformers/validate`.
641
+
642
+ ## quality: Duplicates, bots and a cookieless id
643
+
644
+ - **Purpose:** drop transport resends, send each order to Meta once (London
645
+ prompt 5), score bots, and give Piwik PRO a cookieless visitor id.
646
+ - **Features:**
647
+ - `dedup-by-event-id` at `/flows/server/transformers/dedup/cache`: A resend
648
+ with the same event id is stopped for an hour.
649
+ - `cache-stop` at `/flows/server/transformers/dedup/cache/stop`: stop: true
650
+ turns a cache hit into a drop.
651
+ - `cache-namespace` at `/flows/server/transformers/dedup/cache/namespace`:
652
+ namespace keeps cache keys of different steps apart.
653
+ - `cache-rules` at `/flows/server/transformers/dedup/cache/rules`: rules say
654
+ which events are cached, by which key, for how long.
655
+ - `cache-key` at `/flows/server/transformers/dedup/cache/rules/0/key`: key is
656
+ a list of paths; together they identify a duplicate.
657
+ - `cache-ttl` at `/flows/server/transformers/dedup/cache/rules/0/ttl`: ttl in
658
+ seconds.
659
+ - `order-dedup-meta` at `/flows/server/destinations/meta/cache`: A thank-you
660
+ page reload gets a new event id for the same order; Meta gets each order
661
+ once.
662
+ - `cache-rule-match` at `/flows/server/destinations/meta/cache/rules/0/match`:
663
+ A rule match limits the cache to order complete.
664
+ - `bot-score` at `/flows/server/transformers/bot`: bot annotates user.botScore
665
+ and botCategory from the request; it never drops.
666
+ - `event-filter-bot` at
667
+ `/flows/server/transformers/eventFilter/next/match/or/1`: A botScore above
668
+ 50 keeps the event from the vendors.
669
+ - `fingerprint` at `/flows/server/transformers/fingerprint`: A cookieless
670
+ visitor id rotating daily, for Piwik PRO; it is not hashing of personal
671
+ data.
672
+ - `piwik-visitor-id` at
673
+ `/flows/server/destinations/piwikpro/config/settings/visitorId`: Device,
674
+ then session, then the cookieless hash: the first one present is the visitor
675
+ id.
676
+ - `piwikpro` at `/flows/server/destinations/piwikpro`: Piwik PRO from the
677
+ server, EU hosted, with the cookieless id as fallback.
678
+ - **Fingerprint is a cookieless visitor id for Piwik PRO:** a daily rotating
679
+ HMAC over the anonymized IP, browser and site; it is not hashing of personal
680
+ data.
681
+ - **CLI:** fingerprint with request context, and the collector chain with the
682
+ bot result mocked as a likely bot:
122
683
 
123
- | Feature | Location | Example |
124
- | ----------------------- | ------------- | ------------------------------------------------- |
125
- | Primary source | browser | `"primary": true` |
126
- | Multiple sources | web flow | browser + dataLayer + demo |
127
- | Source-level mapping | dataLayer | `"mapping": { "gtag": { "add_to_cart": {...} } }` |
128
- | Pre-collector chain | dataLayer | `"next": "enricher"` |
129
- | Pre-collector array | http (server) | `"next": ["filter"]` (array form) |
130
- | Require (deferred init) | dataLayer | `"require": ["session"]` |
131
- | Demo source events | demo | Pre-configured test events |
684
+ ```bash
685
+ walkeros push packages/cli/examples/flow-complete.json -f server -e '{"name":"page view","data":{"title":"Home"}}' --ingest '{"ip":"203.0.113.7","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/128.0 Safari/537.36","origin":"https://www.example.com"}' --simulate transformer.fingerprint
686
+ walkeros push packages/cli/examples/flow-complete.json -f server -e '{"name":"page view","data":{"title":"Home"}}' --simulate collector.default --mock collector.next.bot='{"name":"page view","data":{"title":"Home"},"user":{"botScore":80,"botCategory":"automation"}}'
687
+ ```
132
688
 
133
- #### Transformers
689
+ - **MCP:** `flow_simulate`, `package_get` (`@walkeros/server-transformer-bot`,
690
+ `@walkeros/server-transformer-fingerprint`).
691
+ - **In GTM:** a custom template with a Firestore lookup for order dedup; no
692
+ built-in bot score.
693
+ - **Docs:** `collector/cache`, `transformers/bot`, `transformers/fingerprint`.
694
+
695
+ ## state-stores: State, stores and first-party walker.js
696
+
697
+ - **Purpose:** remember across requests, read from stores, and serve the web
698
+ bundle from the server.
699
+ - **Features:**
700
+ - `flow-stores` at `/flows/server/stores`: Stores hold data the steps read:
701
+ script files and customer records.
702
+ - `state-set` at `/flows/server/transformers/sessionSave/state`: state set
703
+ remembers the gclid and start time of a session in the built-in cache.
704
+ - `state-get` at `/flows/server/transformers/sessionLoad/state`: state get
705
+ restores it on the order, per instance.
706
+ - `state-key` at `/flows/server/transformers/sessionSave/state/key`: The key
707
+ falls back from the session id to the visitor hash.
708
+ - `state-value` at `/flows/server/transformers/sessionSave/state/value`: value
709
+ maps what is stored.
710
+ - `state-store` at `/flows/server/transformers/loadUser/state/store`: store
711
+ reads from a named store instead of the built-in cache.
712
+ - `state-mapping` at `/flows/server/transformers/loadUser/state/mapping`:
713
+ mapping picks fields from the stored row and merges them into the target.
714
+ - `load-user` at `/flows/server/transformers/loadUser`: loadUser puts the
715
+ customer lifetime value and segment on logged-in events.
716
+ - `store-fs` at `/flows/server/stores/assets`: An fs store serves files from a
717
+ folder.
718
+ - `store-file` at `/flows/server/stores/assets/config/file`: file: true
719
+ returns bytes exactly as stored.
720
+ - `store-customers` at `/flows/server/stores/customers`: Customer records come
721
+ from an fs store with fake demo data; in production a Sheets or GCS store
722
+ takes its place, loadUser stays as it is.
723
+ - `store-cache` at `/flows/server/stores/customers/cache`: Store cache
724
+ memoizes customer reads for five minutes.
725
+ - `file-transformer` at `/flows/server/transformers/file`: walker.js is served
726
+ first party from the server flow.
727
+ - `file-headers` at `/flows/server/transformers/file/config/settings/headers`:
728
+ Default response headers for served files: caching and resource policy.
729
+ - `gclid-to-ads` at
730
+ `/flows/server/destinations/datamanager/config/mapping/order/complete/data/map/gclid`:
731
+ The gclid saved at session start reaches Google Ads with the purchase.
732
+ - **Built-in `__cache`:** `sessionSave` and `sessionLoad` omit `store`, so they
733
+ use the LRU cache every collector creates. It is per instance: a second
734
+ instance does not see it; a shared store is a later build.
735
+ - **`$store` and store `cache`:** `$store.assets` injects the fs store into
736
+ `file`; the `customers` store memoizes customer reads for five minutes.
737
+ - **Customers:** `customers` is an fs store over fake demo data
738
+ (`packages/cli/examples/customers`, one JSON record per customer id), outside
739
+ root `include`, so no customer data ships in a bundle. `basePath` is relative
740
+ to the working directory, so `CUSTOMERS_DIR` holds it as an absolute path (the
741
+ setup block at the top exports it). In production a Sheets or GCS store takes
742
+ its place; `loadUser` does not change.
743
+ - **walker.js first party:** the `file` transformer answers `GET /walker.js`
744
+ from the `assets` store with `settings.headers` (`Cache-Control`,
745
+ `Cross-Origin-Resource-Policy`) and ends with `next: { stop: true }`. Express
746
+ `settings.cors` adds the CORS headers.
747
+ - **Why `X-Content-Type-Options: nosniff` is not in the file:** express sets it
748
+ on every response itself.
749
+ - The `file` example is proven over HTTP in the integration test; its simulation
750
+ is still waiting.
751
+ - **CLI:** a known customer gets its lifetime value and segment
752
+ (`user.ltv: 420`, `user.segment: "loyal"`):
753
+
754
+ ```bash
755
+ walkeros push packages/cli/examples/flow-complete.json -f server -e '{"name":"order complete","data":{"id":"ORD-100","total":149.8,"currency":"EUR"},"user":{"id":"cust-42"}}' --simulate transformer.loadUser
756
+ ```
757
+
758
+ - Build the web bundle straight into the shared folder (web builds ignore root
759
+ `include`):
760
+
761
+ ```bash
762
+ walkeros bundle packages/cli/examples/flow-complete.json -f web -o packages/cli/examples/shared/walker.js
763
+ ```
764
+
765
+ - **MCP:** `flow_bundle`, `package_get` (`@walkeros/server-store-fs`; for the
766
+ production swap `@walkeros/server-store-sheets`).
767
+ - **In GTM:** a server container can serve `gtm.js` first party; there is no
768
+ general store.
769
+ - **Docs:** `collector/state`, `collector/cache`, `transformers/file`.
770
+
771
+ ## Part IV: ship and operate
772
+
773
+ ## delivery: Reliable delivery and the warehouse
774
+
775
+ - **Purpose:** every event raw into Pub/Sub, a warehouse flow into BigQuery,
776
+ vendors with breakers and timeouts.
777
+ - **Features:**
778
+ - `collect-api` at `/flows/web/destinations/collect`: collect sends every
779
+ event to the first-party server.
780
+ - `collect-transform` at
781
+ `/flows/web/destinations/collect/config/settings/transform`: transform trims
782
+ the payload before it leaves the browser.
783
+ - `pubsub-raw` at `/flows/server/destinations/pubsub`: Pub/Sub gets every
784
+ event raw, impressions, bots and violations included.
785
+ - `destination-setup` at `/flows/server/destinations/pubsub/config/setup`:
786
+ setup provisions the topic, EU storage regions included.
787
+ - `destination-credentials` at
788
+ `/flows/server/destinations/datamanager/config/credentials`:
789
+ config.credentials takes the service account from $secret.
790
+ - `breaker` at `/flows/server/destinations/pubsub/config/breaker`: breaker
791
+ pauses a failing destination instead of hammering it.
792
+ - `timeout` at `/flows/server/destinations/pubsub/config/timeout`: timeout
793
+ bounds each push.
794
+ - `dlq-max` at `/flows/server/destinations/pubsub/config/dlqMax`: dlqMax
795
+ bounds the dead letter queue of failed events.
796
+ - `batch` at `/flows/warehouse/destinations/bigquery/config/batch`: batch
797
+ buffers rows by size and age.
798
+ - `web-batch` at `/flows/web/destinations/collect/config/batch`: The browser
799
+ sends events in batches.
800
+ - `rule-batch` at
801
+ `/flows/web/destinations/collect/config/mapping/product/impression/batch`:
802
+ Impressions get their own, bigger buffer.
803
+ - `queue-backfill` at `/flows/web/destinations/gtm/config/queue`: queue
804
+ backfills events from before the destination started, not a retry queue.
805
+ - `destination-require` at `/flows/web/destinations/gtm/config/require`:
806
+ require starts GTM only after the consent decision.
807
+ - `meta-capi` at `/flows/server/destinations/meta`: Meta Conversions API from
808
+ the server for add to cart and purchase.
809
+ - `datamanager` at `/flows/server/destinations/datamanager`: Google Ads
810
+ conversions through the Data Manager API.
811
+ - `warehouse-flow` at `/flows/warehouse`: A separate flow reads the topic into
812
+ BigQuery; the warehouse keeps everything.
813
+ - `pubsub-pull` at `/flows/warehouse/sources/pubsub`: The pull source reads
814
+ the subscription and acks after the push.
815
+ - `source-setup` at `/flows/warehouse/sources/pubsub/config/setup`: Source
816
+ setup provisions the subscription with a dead letter topic.
817
+ - `source-credentials` at
818
+ `/flows/warehouse/sources/pubsub/config/credentials`: Sources take
819
+ credentials too.
820
+ - `bigquery` at `/flows/warehouse/destinations/bigquery`: Every event becomes
821
+ one BigQuery row.
822
+ - **`queue` is backfill, not retry:** on `gtm`, events from before the consent
823
+ decision are delivered once `require` is met.
824
+ - **`dlqMax`** bounds the dead letter queue of events that failed; `breaker`
825
+ pauses a failing destination; `timeout` bounds each push.
826
+ - **The warehouse keeps everything:** bots, impressions and contract violations
827
+ land in BigQuery; filtering is a vendor decision.
828
+ - **CLI:** Pub/Sub and BigQuery on their mocks, then provisioning (needs GCP,
829
+ not run for this guide):
830
+
831
+ ```bash
832
+ walkeros push packages/cli/examples/flow-complete.json -f server -e events/order-complete.json --simulate destination.pubsub
833
+ walkeros push packages/cli/examples/flow-complete.json -f warehouse -e '{"name":"page view","data":{"title":"Home"},"source":{"type":"express"}}' --simulate destination.bigquery
834
+ walkeros setup destination.pubsub -c packages/cli/examples/flow-complete.json -f server
835
+ ```
836
+
837
+ - Output: `call PubSub.topic.publishMessage(...)` with the event, and
838
+ `call JSONWriter.appendRows(...)` with one row.
839
+ - **MCP:** `flow_simulate`, `package_get` (`@walkeros/server-destination-gcp`).
840
+ - **In GTM:** a server container with a BigQuery or Pub/Sub custom tag.
841
+ - **Docs:** `getting-started/flow/index`.
842
+
843
+ ## build-run: Build and run
844
+
845
+ - **Purpose:** turn the file into artifacts: one `walker.js`, one server
846
+ artifact per server flow.
847
+ - **Features:**
848
+ - `bundle-packages` at `/flows/server/config/bundle/packages`: bundle.packages
849
+ lists what a flow is built from.
850
+ - `package-version` at
851
+ `/flows/server/config/bundle/packages/@walkeros~1collector/version`:
852
+ validate --strict warns for a listed package without a version; an unlisted
853
+ step package is added as latest.
854
+ - `root-include` at `/include`: include ships the shared folder, walker.js
855
+ included, with the server artifact.
856
+ - **Package versions:** every `@walkeros/*` package releases at one version, and
857
+ this file pins all of them to the version it ships with. Exact is recommended.
858
+
859
+ | `version` | Resolves to |
860
+ | ---------------------- | -------------------------------------------------------------- |
861
+ | omitted | `latest`; `validate --strict` warns |
862
+ | `latest` or a dist-tag | the version the tag points to at build time, re-resolved daily |
863
+ | a range (`^4.6.0`) | the highest published version in the range, re-resolved daily |
864
+ | exact (`4.6.1`) | that version, cached for good |
865
+ | `path` instead | a local directory, copied from disk, no registry |
866
+
867
+ - `--release` stamps the id on every event as `source.release`; there is no
868
+ release variable in the file.
869
+ - Root `include` ships `shared/` (with `walker.js`) next to every artifact.
870
+ - **CLI:**
871
+
872
+ ```bash
873
+ walkeros bundle packages/cli/examples/flow-complete.json -f web --stats
874
+ walkeros bundle packages/cli/examples/flow-complete.json --all -o dist/all
875
+ walkeros bundle packages/cli/examples/flow-complete.json -f server -o dist/server.mjs --release 2026-09-26
876
+ runneros start dist/server.mjs -p 8080 --env-file .env
877
+ walkeros cache info
878
+ ```
879
+
880
+ - `--all` needs `-o`: it writes `dist/all/web/walker.js`,
881
+ `dist/all/server/flow.mjs` and `dist/all/warehouse/flow.mjs`.
882
+ - `runneros` (`@walkeros/runner`) runs a prebuilt artifact; the same artifact
883
+ runs in Docker.
884
+ - **MCP:** `flow_bundle`.
885
+ - **In GTM:** publishing a container version.
886
+ - **Docs:** `getting-started/flow/index`.
887
+
888
+ ## operate: Observe, CI and deploy
889
+
890
+ - **Purpose:** watch live events, gate changes in CI, deploy through the app.
891
+ - **Features:**
892
+ - `observe-public` at `/flows/web/config/observe`: A web flow reports to
893
+ Observe through a public url and binding.
894
+ - `observe-level` at `/flows/server/config/observe/level`: level standard
895
+ reports step outcomes; trace adds payloads.
896
+ - `observe-sample` at `/flows/server/config/observe/sample`: sample reports
897
+ one event in ten.
898
+ - `collector-logger` at `/flows/server/collector/logger`: The log level comes
899
+ from the flow variable, so LOG_LEVEL changes it per environment.
900
+ - **CI:** `walkeros validate packages/cli/examples/flow-complete.json --strict`
901
+ plus the step examples as fixtures.
902
+ - **CLI (app-bound, need a login; listed, not run for this guide):**
903
+
904
+ ```bash
905
+ walkeros flows create flow-complete -c packages/cli/examples/flow-complete.json
906
+ walkeros deploy create packages/cli/examples/flow-complete.json
907
+ walkeros deploy start <flowId>
908
+ walkeros observe start <flowId>
909
+ ```
134
910
 
135
- | Feature | Location | Example |
136
- | ----------------------- | --------- | ---------------------------------- |
137
- | Fingerprint transformer | server | Hash context fields to `user.hash` |
138
- | Post-collector chain | Meta | `"before": "fingerprint"` |
139
- | Pre-collector chain | dataLayer | `"next": "enricher"` |
140
-
141
- #### Destinations
142
-
143
- | Feature | Location | Example |
144
- | ----------------------- | ---------------- | ------------------------------------------- |
145
- | Require (deferred init) | GA4 | `"require": ["consent", "user"]` |
146
- | Destination consent | GA4 | `"consent": { "marketing": true }` |
147
- | Destination mapping | All destinations | Entity/action to vendor events |
148
- | Multiple destinations | Both flows | GA4 + API, Meta + Demo |
149
- | Batch option | API | `"batch": 5` |
150
- | Transform function | API | `"transform": "$code:(data) => ..."` |
151
- | Empty consent (always) | debug | `"consent": {}` (fires without consent) |
152
- | Inline `validate:` | GA4 | Step-level schema check on `order complete` |
153
-
154
- #### Collector
155
-
156
- | Feature | Location | Example |
157
- | ----------------- | ---------------- | --------------------------------------- |
158
- | Tagging | Both collectors | `"tagging": 1` |
159
- | Consent defaults | Both collectors | `"consent": { "functional": true }` |
160
- | Globals | Both collectors | `"environment": "demo"` |
161
- | Custom properties | web collector | `"custom": { "campaign": "flow-demo" }` |
162
- | User defaults | web collector | `"user": { "id": "anonymous" }` |
163
- | Dynamic global | server collector | `"startedAt": "$code:Date.now()"` |
164
-
165
- #### Server-Specific
166
-
167
- | Feature | Location | Example |
168
- | --------------------- | --------------- | ----------------------------------------------------------------------- |
169
- | Ingest metadata | http source | `"context.ip": "ip"` |
170
- | Language header | ingest | `"context.language": "headers.accept-language"` |
171
- | Ingest computed field | http source | `"fn": "$code:(ip) => ip.replace(...)"` |
172
- | Policy | Meta | Pre-processing field transformation |
173
- | Policy consent-gated | Meta | `"user_data.em"` with consent |
174
- | Policy nested map | Meta | `"custom_data.request_meta": { "map": {...} }` |
175
- | Local package path | server packages | `"path": "../../core"` (resolve from filesystem) |
176
- | Source cache | http source | `"cache": { "rules": [...] }` (built-in tier) |
177
- | Cache match rule | http source | `"match": { "key": "ingest.method", "operator": "eq", "value": "GET" }` |
178
- | Cache TTL | http source | `"ttl": 300` (seconds) |
179
- | Cache response update | http source | `"update": { "headers.X-Cache": { "key": "cache.status" } }` |
180
-
181
- #### Browser Source
182
-
183
- | Feature | Location | Example |
184
- | --------------- | -------- | ---------------------------------- |
185
- | Prefix | browser | `"prefix": "data-elb"` |
186
- | Auto pageview | browser | `"pageview": true` |
187
- | Session consent | browser | `"session": { "consent": {...} }` |
188
- | ELB binding | browser | `"elb": "elb"`, `"elbLayer": true` |
189
-
190
- ---
191
-
192
- ### Features NOT Used (6)
193
-
194
- #### Now Available via $code: Prefix ✅
195
-
196
- These features are now fully supported in JSON via `$code:` prefix (and ARE used
197
- in this example):
198
-
199
- | Feature | Status |
200
- | ----------------------- | --------------------------------- |
201
- | `fn:` function | ✅ Used via `$code:` in GA4 value |
202
- | `condition:` | ✅ Used via `$code:` in variables |
203
- | Custom transformer code | ✅ Used in enricher, filter |
204
- | Custom destination code | ✅ Used in debug logger |
205
-
206
- #### Omitted for Clarity (6)
207
-
208
- These features could be added but were omitted to keep the example focused:
209
-
210
- | Feature | Why Omitted |
211
- | ------------------------- | ----------------------------- |
212
- | Multiple named flows (3+) | Two flows sufficient for demo |
213
- | Queue config | Advanced batching scenario |
214
- | Retry config | Advanced error handling |
215
- | Custom fetch options | API destination advanced |
216
- | Custom headers in API | Would add complexity |
217
-
218
- ---
219
-
220
- ## Data Flow Examples
221
-
222
- ### Order Complete (Server Flow)
223
-
224
- 1. **Ingest**: Request metadata extracted (IP, user-agent, referer) to
225
- `context.*`
226
- 2. **Policy**: Pre-processes event:
227
- - `user_data.em` from `user.email` (only if marketing consent)
228
- - `user_data.external_id` from `user.id`
229
- - `custom_data.server_processed` = `true`
230
- - `custom_data.request_meta` = `{ ip, ua }` from context
231
- 3. **Fingerprint**: Hashes context fields to `user.hash`
232
- 4. **Mapping**: Transforms to Meta format:
233
- - `"name": "Purchase"`
234
- - `value`, `currency`, `order_id` extracted
235
- - `contents` via `$ref` to definition loop
236
-
237
- ### Product Add (Web Flow)
238
-
239
- 1. **DataLayer Source**: Captures `add_to_cart` event
240
- 2. **Source Mapping**: Transforms to `product add` with walkerOS structure
241
- 3. **Enricher**: Adds enrichedAt timestamp to context
242
- 4. **Collector**: Adds globals, consent, user data
243
- 5. **GA4 Mapping**: Transforms to `add_to_cart` with items array
244
- 6. **API Destination**: Batches and sends to server (if batch size reached)
245
-
246
- ---
247
-
248
- ## Environment Variables
249
-
250
- | Variable | Default | Description |
251
- | -------------------- | ------------------------------- | -------------------------- |
252
- | `GA4_MEASUREMENT_ID` | `G-DEMO123456` | Google Analytics 4 ID |
253
- | `API_URL` | `http://localhost:8080/collect` | Server collection endpoint |
254
- | `META_PIXEL_ID` | `123456789012345` | Meta Pixel ID |
255
- | `META_ACCESS_TOKEN` | `demo_token` | Meta Conversions API token |
911
+ - **MCP:** `flow_manage`, `deploy_manage`, `observe_session`, `flow_validate`
912
+ then `flow_simulate` then `flow_push` as the agent loop.
913
+ - **In GTM:** preview mode and container versions.
914
+ - **Docs:** `collector/logger`.