Skip to main content
Ask your AI

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.

Reference page

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:

  1. version - Schema version (4)
  2. flows - Named flow configurations (each a Flow)

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.

/flows
{
  "web": {
    "config": { … },
    "collector": { … },
    "sources": { … },
    "transformers": { … },
    "destinations": { … }
  },
  "server": {
    "config": { … },
    "variables": { … },
    "collector": { … },
    "stores": { … },
    "sources": { … },
    "transformers": { … },
    "destinations": { … }
  },
  "warehouse": {
    "config": { … },
    "sources": { … },
    "destinations": { … }
  }
}
One file holds three flows: web in the browser, server behind it, warehouse reading the topic. Guide chapter tour

Config 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​

PropertyModeValueWhen to Use
package:Bundled"@walkeros/..." (string)CLI resolves and bundles from npm
code:IntegratedsourceBrowser (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:

/flows/web/config
{
  "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"
    }
  }
}
config holds what the build needs: platform, url, settings, bundle, observe. Guide chapter tour

Platform 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 validate warns about packages that set neither version nor path, and --strict turns that warning into a failing exit code.
  • imports - Array of named exports to import
  • path - Local filesystem path (takes precedence over version)

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 using code:)
  • 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 states
  • policy - 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 using code:)
  • import - Named export of package to use (optional; the package's default export otherwise)
  • code - Inline code object { push, type?, init? }, used instead of package (see Inline Code)
  • config - Transformer-specific configuration
  • env - Environment-specific settings
  • before - 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 } } })"
      }
    }
  }
}
SyntaxBehavior
"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 before on a source for consent-exempt preprocessing (runs before the source's push)
  • Source chain: Use next on a source to route its events through transformers before the collector
  • Collector chain: Use next on the collector to run one chain for every event, after the collector completed it and before the destination fan-out
  • Destination chain: Use before on a destination to run transformers for that destination only
  • Post-push chain: Use next on 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 require is not initialized and receives no events until then. Its events are queued and delivered once require is met, unless the destination sets queue: false: then it receives only the events pushed after that.
  • A source with require initializes and pushes events right away. require only holds back the collector events its on() handler receives (such as consent or run).

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 identifier
  • init - Optional init function with $code: prefix

Rules:

  • Use package OR code, never both
  • config stays 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 event
  • consent - Default consent state
  • next - Collector chain: a Route that runs once per event, after the collector completed it and before the destination fan-out. Every destination receives what it produces; a stop here reaches no destination, and each many copy is delivered to the destinations on its own. Per-destination work stays on destination.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 with walkeros bundle or deploy it as a hosted flow. Accepts a literal or a $var/$env reference; 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/all

Cross-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:

/variables
{
  "currency": "EUR",
  "gcp": {
    "project": "$env.GCP_PROJECT:demo-project",
    "dataset": "$env.BQ_DATASET:walkeros",
    "topic": "$env.PUBSUB_TOPIC:walkeros-events"
  }
}
Root variables are shared by all flows; a flow or a step can override them. Guide chapter 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):

  1. Step level (variables on a source, transformer, destination, or store) - Highest priority
  2. Flow level (flows.<name>.variables) - Middle priority
  3. 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 reads process.env when the bundle runs; an unset variable resolves to undefined. Inside a longer string, it becomes the text undefined.
  • $env.GA4_ID:default - Uses "default" if not set
Why only $env supports defaults

Environment 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 mapping
  • condition: 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 CLI
  • Flow - Single flow configuration (with optional config, sources, destinations, etc.)
  • Flow.Config - Per-flow config block (platform, url, settings, bundle, observe)
  • Collector.InitConfig - Runtime type passed to startFlow()

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:

/flows/web/sources
{
  "usercentrics": { … },
  "session": { … },
  "dataLayer": { … },
  "browser": { … }
}
Sources turn whatever happens (a CMP decision, a dataLayer push, a click) into events. Guide chapter tour
/flows/server/transformers
{
  "ga4Decode": { … },
  "ga4Consent": { … },
  "file": { … },
  "dedup": { … },
  "fingerprint": { … },
  "bot": { … },
  "enrich": { … },
  "loadUser": { … },
  "sessionSave": { … },
  "sessionLoad": { … },
  "validate": { … },
  "eventFilter": { … },
  "pseudonymize": { … }
}
Transformers are named hops that validate, enrich, filter or remember events. Guide chapter tour
/flows/server/destinations
{
  "pubsub": { … },
  "meta": { … },
  "piwikpro": { … },
  "datamanager": { … }
}
Destinations fan out: every event reaches each destination that accepts it. Guide chapter tour

Programmatic 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 events

See 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
💡 Need implementation support?
elbwalker offers hands-on support: setup review, measurement planning, destination mapping, and live troubleshooting. Book a 2-hour session (€399)