Flow
Flow configuration is walkerOS's "configuration as code" approach. A single JSON file defines your entire event collection pipeline, making it portable, version-controlled, and deployable across environments.
This page is the complete Flow configuration reference. If you are new to walkerOS, start with the Quickstart and the GA4 ecommerce tutorial, then come back here to look up specific options.
Configuration structure
A flow configuration uses the Flow.Json format with two required fields:
version- Schema version (4)flows- Named flow configurations (each aFlow)
Example
The examples on this page come from
flow-complete.json,
a tested ecommerce setup with three flows: web in the browser, server
behind it, and warehouse reading the Pub/Sub topic into BigQuery. For a
minimal first flow, start with the Quickstart.
{
"web": {
"config": { … },
"collector": { … },
"sources": { … },
"transformers": { … },
"destinations": { … }
},
"server": {
"config": { … },
"variables": { … },
"collector": { … },
"stores": { … },
"sources": { … },
"transformers": { … },
"destinations": { … }
},
"warehouse": {
"config": { … },
"sources": { … },
"destinations": { … }
}
}tourConfig syntax: package: vs code:
When configuring sources and destinations, you'll see two different syntaxes depending on your operating mode.
Bundled mode (JSON with CLI)
Use package: with a string reference. The CLI downloads and bundles the npm package:
"sources": {
"browser": {
"package": "@walkeros/web-source-browser",
"config": { "settings": { "pageview": true } }
}
}Integrated mode (TypeScript with startFlow)
Use code: with a direct import reference:
import { startFlow } from '@walkeros/collector';
import { sourceBrowser } from '@walkeros/web-source-browser';
const flow = await startFlow({
sources: {
browser: {
code: sourceBrowser,
config: { settings: { pageview: true } }
}
}
});Quick reference
| Property | Mode | Value | When to Use |
|---|---|---|---|
package: | Bundled | "@walkeros/..." (string) | CLI resolves and bundles from npm |
code: | Integrated | sourceBrowser (import) | Direct code reference in your app |
Both achieve the same result. The difference is whether the CLI bundles the code for you (Bundled) or you import it directly (Integrated).
See Operating Modes for more details on choosing your approach.
Flow configuration (Flow)
Each flow in flows is a Flow that defines runtime behavior. Per-flow build/runtime metadata sits inside config (Flow.Config).
Platform
Platform is set explicitly via config.platform:
/flows/web/config/platform: "web". platform decides how a flow is built: a browser bundle or a server artifact. Guide chapter tour
It sits in the flow's config next to the other build and runtime metadata:
{
"platform": "web",
"url": "$env.SITE_URL:https://www.example.com",
"settings": {
"windowCollector": "walkerCollector"
},
"observe": {
"url": "$env.OBSERVE_URL:https://app.walkeros.io/observe",
"binding": "$env.OBSERVE_BINDING:obs-web"
},
"bundle": {
"packages": {
"@walkeros/collector": {
"version": "4.8.0"
},
"@walkeros/web-source-cmp-usercentrics": {
"version": "4.8.0"
},
"@walkeros/web-source-session": {
"version": "4.8.0"
},
"@walkeros/web-source-datalayer": {
"version": "4.8.0"
},
"@walkeros/web-source-browser": {
"version": "4.8.0"
},
"@walkeros/web-destination-gtag": {
"version": "4.8.0"
},
"@walkeros/web-destination-api": {
"version": "4.8.0"
}
},
"env": {
"ENVIRONMENT": "production"
}
}
}tourPlatform options:
"platform": "server"- Node.js server environment (HTTP endpoints, cloud functions)"platform": "web"- Browser environment (client-side tracking)
The CLI automatically applies platform-specific build defaults:
- Web: IIFE format, ES2020 target
- Server: ESM format, Node20 target
The bundle is written to stdout by default. Use -o to write to a file (e.g., -o ./dist/walker.js for web, -o ./dist/bundle.mjs for server).
Bundle
Build-time configuration lives under flows.<name>.config.bundle. The packages field specifies npm packages to download and bundle, and overrides pins transitive dependency versions (npm overrides semantics):
{
"config": {
"platform": "web",
"bundle": {
"packages": {
"@walkeros/web-source-browser": {
"version": "^4",
"imports": ["sourceBrowser"]
},
"@walkeros/web-destination-api": {
"imports": ["destinationAPI"]
}
},
"overrides": {
"@amplitude/analytics-types": "2.11.1"
}
}
}
}packages properties:
version- An exact npm version, a semver range, or a tag. Defaults to"latest". A major range such as"^4"picks up new minor and patch releases without jumping to the next major version.walkeros validatewarns about packages that set neitherversionnorpath, and--strictturns that warning into a failing exit code.imports- Array of named exports to importpath- Local filesystem path (takes precedence overversion)
overrides is a Record<string, string>: for each named package, the version to use for any transitive reference. Direct package specs always win over overrides; overrides only substitute transitive dependencies during resolution.
For development or custom packages, use path to reference a local directory:
{
"config": {
"platform": "web",
"bundle": {
"packages": {
"@my/custom-destination": {
"path": "./my-destination",
"imports": ["myDestination"]
}
}
}
}
}See Local Packages in the CLI documentation for more details.
Step-level package versions
When configuring individual steps (sources, transformers, or destinations), the package field accepts an optional inline version suffix:
{
"sources": {
"browser": {
"package": "@walkeros/web-source-browser@^4",
"config": { "settings": { "pageview": true } }
}
}
}If the same package is pinned in config.bundle.packages to a different version, the bundle-level pin takes precedence and the inline version is ignored (with a build warning).
Sources
Sources capture events from various inputs. Each source needs:
package- The npm package (required for package-based sources, optional when usingcode:)config- Source-specific settings
{
"sources": {
"http": {
"package": "@walkeros/server-source-express",
"config": {
"settings": {
"paths": ["/collect"],
"cors": true
}
}
}
}
}See Sources documentation for all available options.
Destinations
Destinations receive processed events and send them to analytics tools, databases, or APIs:
{
"destinations": {
"bigquery": {
"package": "@walkeros/server-destination-gcp",
"config": {
"settings": {
"projectId": "my-project",
"datasetId": "analytics",
"tableId": "events"
},
"mapping": {
"page": {
"view": {
"name": "page_view"
}
}
}
}
},
"console": {
"package": "@walkeros/destination-demo",
"config": {
"settings": {
"name": "Debug Logger"
}
}
}
}
}Configuration options:
settings- Destination-specific configuration (API keys, endpoints, etc.)mapping- Event transformation rules (see Mapping documentation)consent- Required consent statespolicy- Processing rules
Consent gating:
Gate a destination on consent with config.consent. The destination initializes once one of the listed consent keys is granted; until then its events wait in a queue and are delivered after the grant, during the same run. A denial keeps them waiting:
{
"destinations": {
"ga4": {
"package": "@walkeros/web-destination-gtag",
"config": {
"consent": { "marketing": true },
"settings": { "ga4": { "measurementId": "G-DEMO123456" } }
}
}
}
}require is a separate mechanism that waits for collector events, not for granted consent (see Deferred activation with require).
See Destinations documentation for all available options.
Transformers
Transformers process events between sources and destinations. They validate, enrich, or redact events in the pipeline.
{
"transformers": {
"fingerprint": {
"package": "@walkeros/server-transformer-fingerprint",
"config": {
"settings": { "salt": "$env.FINGERPRINT_SALT" }
}
}
}
}Configuration options:
package- The npm package (required for package-based steps, optional when usingcode:)import- Named export ofpackageto use (optional; the package's default export otherwise)code- Inline code object{ push, type?, init? }, used instead ofpackage(see Inline Code)config- Transformer-specific configurationenv- Environment-specific settingsbefore- Pre-transformer chain (runs before this transformer's push)next- Next transformer in chain (omit to end chain)variables- Transformer-level variable overrides
Chaining transformers:
Link transformers together using next:
{
"transformers": {
"enrich": {
"code": {
"type": "enrich",
"push": "$code:(event) => ({ event: { ...event, data: { ...event.data, enriched: true } } })"
},
"next": "fingerprint"
},
"fingerprint": {
"package": "@walkeros/server-transformer-fingerprint"
}
}
}Explicit chain control with arrays:
For explicit control over the order, use an array instead of a string. The array is the backbone of the chain: its entries run in order, and a member's own next runs right after that member before the array continues:
{
"sources": {
"http": {
"package": "@walkeros/server-source-express",
"next": ["fingerprint", "enrich", "redact"]
}
},
"transformers": {
"fingerprint": { "package": "@walkeros/server-transformer-fingerprint" },
"enrich": {
"code": {
"type": "enrich",
"push": "$code:(event) => ({ event: { ...event, data: { ...event.data, enriched: true } } })"
}
},
"redact": {
"code": {
"type": "redact",
"push": "$code:(event) => ({ event: { ...event, user: { ...event.user, email: undefined } } })"
}
}
}
}| Syntax | Behavior |
|---|---|
"next": "fingerprint" | Runs fingerprint, then follows each transformer's own next |
"next": ["fingerprint", "enrich"] | Runs the array in order; a member's own next is inserted right after that member |
Connecting to sources, the collector, and destinations:
- Source preprocessing: Use
beforeon a source for consent-exempt preprocessing (runs before the source's push) - Source chain: Use
nexton a source to route its events through transformers before the collector - Collector chain: Use
nexton the collector to run one chain for every event, after the collector completed it and before the destination fan-out - Destination chain: Use
beforeon a destination to run transformers for that destination only - Post-push chain: Use
nexton a destination to process events after the destination push completes
The server flow of the complete example uses the source, collector and destination positions, shown step by step on the Routing page.
All chain fields (before and next, including collector.next) accept the same route grammar: a transformer id, an array, or a route config (see Conditional routing).
See Transformers documentation for available transformers and custom transformer guide.
Connection rules
Sources, transformers, the collector and destinations connect through chain fields (before and next). The connection table, chain resolution, route operators and dropping events with stop are on the Routing page.
Deferred activation with require
Sources and destinations support require to wait until specific collector events have fired:
- A destination with
requireis not initialized and receives no events until then. Its events are queued and delivered oncerequireis met, unless the destination setsqueue: false: then it receives only the events pushed after that. - A source with
requireinitializes and pushes events right away.requireonly holds back the collector events itson()handler receives (such asconsentorrun).
For the browser source, require: ["consent"] holds back the page view and element triggers such as load, impression or visible until the first consent choice, granted or denied. click and submit triggers are captured from the start.
{
"sources": {
"browser": {
"package": "@walkeros/web-source-browser",
"config": { "require": ["consent"] }
}
},
"destinations": {
"api": {
"package": "@walkeros/web-destination-api",
"config": {
"require": ["consent"],
"settings": { "url": "https://collect.example.com/events" }
}
}
}
}For require: ["consent"] the collector needs a consent state: the collector.consent default or a walker consent command. Any state counts, granted or denied, so with a consent default set, require is met at startup. require does not check consent values; config.consent does.
Inline Code (without packages)
For simple one-liner logic, define sources, transformers, or destinations inline without creating a package.
Use code object instead of package:
Inline Transformer:
{
"transformers": {
"enrich": {
"code": {
"type": "enricher",
"push": "$code:(event) => ({ event: { ...event, data: { ...event.data, timestamp: Date.now() } } })"
},
"config": {}
}
}
}Inline Destination:
{
"destinations": {
"logger": {
"code": {
"type": "console-logger",
"push": "$code:(event, context) => { context.logger.info(event.name, event.data); }"
},
"config": {}
}
}
}Code object properties:
push- The push function with$code:prefix (required)type- Optional instance type identifierinit- Optional init function with$code:prefix
Rules:
- Use
packageORcode, never both configstays separate (same as package-based)$code:prefix outputs raw JavaScript at bundle time
Collector
The collector processes events from sources and routes them to destinations:
{
"collector": {
"globals": {
"environment": "production"
},
"consent": {
"functional": true
},
"next": ["bot", "session"]
}
}Options:
run- Whether to start the collector automatically (default:true)globals- Properties added to every eventconsent- Default consent statenext- Collector chain: aRoutethat runs once per event, after the collector completed it and before the destination fan-out. Every destination receives what it produces; astophere reaches no destination, and eachmanycopy is delivered to the destinations on its own. Per-destination work stays ondestination.before
See Collector documentation for complete options.
Web-specific options
For browser bundles, you can configure the collector's window variable name via
config.settings:
{
"config": {
"platform": "web",
"settings": {
"windowCollector": "walkerCollector"
}
}
}Properties:
windowCollector- Global variable name for collector instance (default:"walkerOS"). The bundle assigns the collector to this one global, whether you build withwalkeros bundleor deploy it as a hosted flow. Accepts a literal or a$var/$envreference; the resolved value must be a JavaScript identifier and not a reserved name:elb,elbLayer,window,document,location,self,top,parent,frames,globalThis,__proto__,constructor,prototype.
The window.elb function is owned by the browser source: set elb in the
browser source's config.settings to change its global name (default:
"elb"). If the deprecated config.settings.windowElb is set, the CLI forwards its value to
the browser source's settings.elb and logs a warning; if that source already
sets elb, windowElb is ignored.
Multi-flow configuration
One file holds several flows. The complete example splits by platform (web,
server, warehouse, see Example); the same works per environment,
for example development and production.
Build specific flows using the CLI:
# Build the web flow
walkeros bundle flow.json --flow web
# Build the server flow
walkeros bundle flow.json --flow server
# Build all flows (--all needs an output directory, one subfolder per flow)
walkeros bundle flow.json --all -o dist/allCross-flow references with $flow
Flows in the same file can reference each other's config block via $flow.<name>.<path>. The most common case is linking a web flow's destination to a server flow's URL. In the complete example the web collect destination sends to the server flow:
/flows/web/destinations/collect/config/settings/url: "$flow.server.url". $flow.server.url points the browser at the server flow; it is whole-string only. Guide chapter references
Cross-flow refs read values from another flow's config block (platform, url, settings.*), keeping web and server flows in sync without duplication. The bundler resolves $flow.server.url to the server flow's config.url at build time. Validate resolves the reference the same way the bundle does: an unknown flow or a missing key is an error in both. An empty config.url fails the schema check in bundle and in validate without --flow; validate --flow web does not check the server flow and passes. An empty value under settings resolves to an empty string.
Dynamic patterns
Flow configurations support these dynamic patterns for reusable, environment-aware configs (see Reference syntax for every reference type):
$var.name - Config Variables
Reference variables defined in variables for shared values across your config:
{
"currency": "EUR",
"gcp": {
"project": "$env.GCP_PROJECT:demo-project",
"dataset": "$env.BQ_DATASET:walkeros",
"topic": "$env.PUBSUB_TOPIC:walkeros-events"
}
}references/flows/web/destinations/ga4/config/policy/data.currency/1/value: "$var.currency". A whole-string $var keeps the variable type. Guide chapter references
/flows/warehouse/sources/pubsub/config/setup/deadLetterPolicy/deadLetterTopic: "projects/$var.gcp.project/topics/walkeros-dead-letter". Inside a longer string $var is replaced in place, here in a full topic name. Guide chapter references
Variables can be defined at three levels (higher specificity wins):
- Step level (
variableson a source, transformer, destination, or store) - Highest priority - Flow level (
flows.<name>.variables) - Middle priority - Root level (top-level
variables) - Lowest priority
$env.NAME - Environment Variables
Reference environment variables with optional defaults:
/variables/gcp/project: "$env.GCP_PROJECT:demo-project". $env.NAME:default works out of the box and takes the environment value when set. Guide chapter references
/flows/server/transformers/fingerprint/config/settings/salt: "$env.FINGERPRINT_SALT". $env without a default must be set: a secret salt never gets a public default. Guide chapter references
Syntax:
$env.GA4_ID- Required. A web flow reads the value at bundle time, and the bundle fails if it is not set. A server flow readsprocess.envwhen the bundle runs; an unset variable resolves toundefined. Inside a longer string, it becomes the textundefined.$env.GA4_ID:default- Uses "default" if not set
$env supports defaultsEnvironment variables are external and unpredictable - they might not be set in all environments. Config variables ($var) are explicitly defined, so a missing one indicates a configuration error.
$code: - Inline JavaScript
Embed JavaScript code directly in JSON config values:
{
"destinations": {
"api": {
"package": "@walkeros/web-destination-api",
"config": {
"mapping": {
"product": {
"view": {
"data": {
"map": {
"item_id": {
"fn": "$code:(event) => String(event.data.id).toUpperCase()"
}
}
}
}
}
}
}
}
}
}The $code: prefix is stripped during bundling, outputting raw JavaScript:
// Bundler output
{
item_id: {
fn: (event) => String(event.data.id).toUpperCase()
}
}A fn receives the value being mapped (here the whole event, inside a loop
the item) and returns the result, so it reads the field itself. A fn next to a
key never runs: each value has one producer.
Use cases:
fn:callbacks - Transform values in mappingcondition:predicates - Conditional event processing- Custom logic - Any inline function or expression
Syntax notes:
- Multi-line code: Use JSON escapes (
\n,\") - Scope: Same as bundled code (access to imports and variables)
- Errors: esbuild rejects syntax errors at build time. The code must be plain JavaScript (TypeScript annotations fail the bundle) and is not type-checked.
Type hierarchy
walkerOS uses a clear type hierarchy:
┌─────────────────────────────────────────────────────────────────┐
│ Flow.Json (config file) │
│ ├── version: 4 │
│ ├── variables?: { currency: "EUR" } │
│ └── flows: │
│ └── default: Flow │
└─────────────────────────────────────────────────────────────────┘
│
│ CLI resolves $var, $env, $flow, $contract, $secret
▼
┌─────────────────────────────────────────────────────────────────┐
│ Flow (resolved flow) │
│ ├── config: { platform, url?, settings?, bundle? } │
│ ├── sources: { ... } │
│ ├── transformers: { ... } │
│ ├── destinations: { ... } │
│ ├── stores: { ... } │
│ └── collector: { next?, globals?, ... } │
└─────────────────────────────────────────────────────────────────┘
│
│ CLI bundles and transforms
▼
┌─────────────────────────────────────────────────────────────────┐
│ Collector.InitConfig (runtime) │
│ Passed to startFlow() at runtime │
└─────────────────────────────────────────────────────────────────┘
Flow.Json- Root config file format for CLIFlow- Single flow configuration (with optionalconfig,sources,destinations, etc.)Flow.Config- Per-flow config block (platform,url,settings,bundle,observe)Collector.InitConfig- Runtime type passed tostartFlow()
Complete example
The canonical, tested flow is
packages/cli/examples/flow-complete.json,
shown in parts on this page and across the docs. Its
guide
teaches it in 15 chapters. @walkeros/cli/examples exports the file and its
feature manifest.
Every flow declares its steps in named maps. The web flow's sources, and the server flow's transformers and destinations:
{
"usercentrics": { … },
"session": { … },
"dataLayer": { … },
"browser": { … }
}tour{
"ga4Decode": { … },
"ga4Consent": { … },
"file": { … },
"dedup": { … },
"fingerprint": { … },
"bot": { … },
"enrich": { … },
"loadUser": { … },
"sessionSave": { … },
"sessionLoad": { … },
"validate": { … },
"eventFilter": { … },
"pseudonymize": { … }
}tour{
"pubsub": { … },
"meta": { … },
"piwikpro": { … },
"datamanager": { … }
}tourProgrammatic usage
You can also use configuration programmatically with the startFlow function:
import { startFlow } from '@walkeros/collector';
import { sourceExpress } from '@walkeros/server-source-express';
import { destinationDemo } from '@walkeros/destination-demo';
const { collector, elb } = await startFlow({
sources: {
http: {
code: sourceExpress,
config: {
settings: { paths: ['/collect'], port: 8080 }
}
}
},
destinations: {
console: {
code: destinationDemo,
config: {
settings: { name: 'Logger' }
}
}
},
});
// Collector is now running and ready to process eventsSee the Collector documentation for complete API reference.
Next steps
- CLI - Learn how to bundle and test flows
- Runtime - Run built flows in containers
- Sources - Explore available event sources
- Destinations - Configure analytics destinations
- Mapping - Transform events for destinations