walkerOS CLI
The walkerOS CLI (@walkeros/cli) is a command-line tool for building, testing, and running event collection flows. It handles the complete workflow from configuration to deployment. It bundles flows into optimized JavaScript, testing with simulated events, and running collection servers locally or in Docker.
Installation
Global Installation (Recommended)
Install globally to use the walkeros command anywhere:
npm install -g @walkeros/cli
# Verify installation
walkeros --versionLocal Installation
Install in your project for team consistency:
npm install --save-dev @walkeros/cli
# Use with npx
npx walkeros --versionCommands overview
| Command | Purpose | Use Case |
|---|---|---|
bundle | Build production-ready bundle from flow config | Create deployable JavaScript from configuration |
push | Execute event with real API calls (a web flow records them), or --simulate for mocked | Testing and production validation |
setup | Run the optional setup() lifecycle on one component | Provision external resources (BigQuery datasets, Pub/Sub topics, SQLite tables, webhook registrations) |
validate | Validate events, flows, mappings, contracts, or entries | Check configuration before bundling |
cache | Manage CLI package and build caches | Clear stale caches, view cache statistics |
auth | Authentication and identity | Log in, log out, check identity |
projects | Manage walkerOS projects | Create, list, update, delete projects |
flows | Manage walkerOS flows | Create, list, update, delete, duplicate flows |
deploy | Create and manage deployments | Deploy flows through the walkerOS app or self-hosted |
previews | Manage preview bundles | Test flow changes on live sites before deploying |
observe | Open an Observe session on a flow | Watch a running flow's live events from the terminal |
diagnostics | Show the CLI version, the app URL, app health, and the API contract verdict | See which backend you are on and whether this CLI matches it |
The push command accepts either a config JSON or a pre-built bundle as input.
Configuration types
The CLI uses types from @walkeros/core:
Flow.Json- Root config file format (version,flows)Flow- Single flow (hasconfig,sources,destinations,transformers,collector)Flow.Config- Per-flow config block (platform,url,settings,bundle)Collector.InitConfig- Runtime type passed tostartFlow()
The CLI transforms Flow.Json → Flow (per flow) → bundled code that uses Collector.InitConfig at runtime.
Packages and overrides
Build-time concerns live under each flow's config.bundle block. Three fields are supported:
packages: the list of npm packages (or local paths) the bundler should install for this flow. Pacote downloads them based on this list. You do not need to runnpm installyourself for step packages; the CLI handles it transparently.overrides: pin transitive dependency versions, matching npm'soverridessemantics. Useful when an upstream package declares an overly narrow version range that conflicts with a newer transitive version in the same tree.traceInclude(server flows only): an escape hatch for the file tracer. See below.
"flows": {
"default": {
"config": {
"platform": "server",
"bundle": {
"packages": {
"@walkeros/collector": {}
},
"overrides": {
"@amplitude/analytics-types": "2.11.1"
}
}
}
}
}Server bundles use nft tracing
Server flows (platform: "server") are traced automatically with @vercel/nft. The bundler externalizes every step package, then nft walks the entry to figure out which files are actually reachable at runtime, and copies only those files into dist/node_modules/. Native add-ons, .proto files, and __dirname-loaded assets are picked up.
There is no walkerOS.bundle.external annotation: nft figures out the externalization automatically.
After tracing, the bundler checks every package in bundle.packages that the bundle imports (a step's package, or a package exposed through imports). One missing from the trace fails the build (nft-trace: resolved packages missing from trace: ...). A declared package that the bundle does not import and no traced package pulls in (for example one a step package needs for its types only) is not in the bundle, and the build only warns:
Package @walkeros/server-core is declared in bundle.packages but nothing imports it; it is not in the bundle. Remove it from bundle.packages if unused.
Escape hatch: traceInclude
If nft cannot statically reach a file (rare: dynamic require(somePath) constructed from a runtime variable, dynamically loaded data files), declare it explicitly with flow.<name>.config.bundle.traceInclude. Paths and globs are both supported (matched via picomatch). Paths resolve against the install root, not your project directory.
"flows": {
"default": {
"config": {
"platform": "server",
"bundle": {
"packages": { "@walkeros/server-destination-gcp": {} },
"traceInclude": [
"node_modules/some-pkg/data/*.json",
"node_modules/another-pkg/lib/runtime-loaded.js"
]
}
}
}
}Range conflict resolution
When two transitive consumers declare incompatible ranges for the same dependency (e.g., arrify@^3.0.0 from one package, arrify@^2.0.0 from another), the bundler now resolves the chosen range to a concrete version via the npm registry and nests any non-satisfying spec under its consumer's node_modules/. This kills classes of upstream chaos (like arrify is not a function from @google-cloud/common) before they can reach runtime. After install, a defense-in-depth check warns when any installed package's declared deps don't match what's reachable on its resolution path; if you see those warnings, add an overrides entry to your flow's config.bundle.overrides to pin the offending dep. If the npm registry is unreachable during validation, set BUNDLER_STRICT_RANGES=0 to bypass strict validation with a warning instead of failing the build.
Schema version
The flow.json schema stays at version: 4. Build-time fields live under flow.<name>.config.bundle.{packages, overrides, traceInclude}. The flow.<name>.config.bundle.external sub-field is no longer supported in @walkeros/cli@4.x.
Package imports
Implicit collector
The collector is added automatically. To pin a specific version (recommended for production), add the collector explicitly:
"packages": {
"@walkeros/collector": { "version": "4.7.0" },
"@walkeros/web-source-browser": {},
"@walkeros/destination-demo": {}
}Default exports (sources & destinations)
Sources and destinations automatically use their default export - no imports needed:
"packages": {
"@walkeros/web-source-browser": {}, // Uses default export
"@walkeros/destination-demo": {} // Uses default export
}Utility imports
Use imports when you need specific utility functions from a package:
"packages": {
"@walkeros/core": { "imports": ["getId", "clone"] }, // Optional utilities
"@walkeros/web-source-browser": {
"imports": ["createTagger"] // Additional utility
}
}Named-export selection with import
For packages without a default export, or to pick a specific named export, set import on the step alongside package. The bundler adds the name to the imports list automatically.
"sources": {
"custom": {
"package": "@my/custom-package",
"import": "namedExport"
}
}The legacy code: "namedExport" form is no longer accepted. Validation raises OBSOLETE_CODE_STRING with a rename hint pointing to import.
Local packages
By default, the CLI downloads packages from npm. For development or testing unpublished packages, you can use local packages instead by specifying a path property.
Configuration
Add a path property to any package to use a local directory instead of npm:
{
"packages": {
"@my/custom-destination": {
"path": "./my-custom-destination"
}
}
}Resolution Rules
pathtakes precedence - When bothpathandversionare specified,pathis used- Relative paths - Resolved relative to the config file's directory
- Absolute paths - Used as-is
- dist folder - If a
dist/folder exists, it's used; otherwise the package root is used
Dependency Resolution
When a local package has dependencies on other packages that are also specified with local paths, the CLI will use the local versions for those dependencies too. This prevents npm versions from overwriting your local packages.
{
"packages": {
"@walkeros/core": {
"path": "../packages/core"
},
"@walkeros/collector": {
"path": "../packages/collector"
}
}
}In this example, even though @walkeros/collector depends on @walkeros/core, the local version of core will be used (not downloaded from npm). This is essential when testing changes across multiple interdependent packages.
Use Cases
Development of custom packages:
{
"packages": {
"@my-org/destination-custom": {
"path": "../my-destination"
}
}
}Testing local changes to walkerOS packages:
{
"packages": {
"@walkeros/collector": {
"path": "../../packages/collector"
}
}
}When ready for production, simply remove the path property to use the published npm version.
Getting started
Before using the CLI, you need a flow configuration file. Here's a minimal example:
{
"version": 4,
"flows": {
"default": {
"config": {
"platform": "server",
"bundle": {
"packages": {
"@walkeros/server-source-express": { "version": "^4" },
"@walkeros/destination-demo": { "version": "^4" }
}
}
},
"sources": {
"http": {
"package": "@walkeros/server-source-express",
"config": {
"settings": { "paths": ["/collect"] }
}
}
},
"destinations": {
"console": {
"package": "@walkeros/destination-demo",
"config": {
"settings": { "name": "Console Logger" }
}
}
},
"collector": { "run": true }
}
}
}The @walkeros/collector package is automatically added when your flow has sources or destinations.
Save this as flow.json.
Bundle command
The bundle command builds production-ready JavaScript bundles from flow configurations.
Use Case
You've defined your sources, destinations, and transformations in a flow configuration file. Now you need to:
- Download the required npm packages
- Bundle everything into a single optimized JavaScript file
- Deploy it to production (Docker, Cloud Run, serverless functions)
The bundle command handles all of this.
Basic Usage
walkeros bundle flow.jsonThis writes the optimized bundle to stdout (web flows only). Use -o to write to a file or directory (e.g., -o ./dist/walker.js for web, -o ./dist/ for server, which produces dist/{flow.mjs, package.json, node_modules/}). For server flows you can also write a .tar.gz/.tgz archive (e.g., -o ./flow.tar.gz), which packs the server bundle directory (flow.mjs, package.json, node_modules/) into a single gzip file. Web single-file bundles do not support archive output.
Step-by-Step Guide
1. Create a flow configuration
Create server-collect.json:
{
"version": 4,
"flows": {
"default": {
"config": {
"platform": "server",
"bundle": {
"packages": {
"@walkeros/server-source-express": { "version": "^4" },
"@walkeros/destination-demo": { "version": "^4" }
}
}
},
"sources": {
"http": {
"package": "@walkeros/server-source-express",
"config": {
"settings": {
"paths": ["/collect"],
"cors": true
}
}
}
},
"destinations": {
"console": {
"package": "@walkeros/destination-demo",
"config": {
"settings": {
"name": "Event Logger",
"values": ["name", "data.title", "timestamp"]
}
}
}
},
"collector": { "run": true }
}
}
}2. Bundle the flow
walkeros bundle server-collect.json --statsOutput:
📦 Downloading packages from npm...
✓ @walkeros/collector@latest
✓ @walkeros/server-source-express@latest
✓ @walkeros/destination-demo@latest
🔨 Bundling...
✓ Bundle created: ./dist/flow.mjs
📊 Bundle Statistics:
Size: 45.2 KB (minified)
Packages: 3
Format: ESM
3. Review the bundle
ls -lh dist/
# -rw-r--r-- 1 user staff 45K flow.mjs
# -rw-r--r-- 1 user staff 152B package.json
# drwxr-xr-x 12 user staff 384B node_modules
file dist/flow.mjs
# dist/flow.mjs: JavaScript source, UTF-8 Unicode textThe bundle is now ready to deploy!
Options
walkeros bundle <config-file> [options]| Option | Description |
|---|---|
-o, --output <path> | Write bundle to file, directory, or presigned URL |
-f, --flow <name> | Build specific flow (for multi-flow configs) |
--all | Build all flows |
--stats | Show bundle statistics |
--json | Output statistics as JSON (for CI/CD) |
--no-cache | Skip package cache, download fresh |
--release <id> | Config release id, stamped on event.source.release. Beats a collector.release in the flow. Default: a content id of the config, the resolved packages and the CLI version. Same input, same release. Env and secret values are not part of it, only their names |
--manifest [source] | Build from a manifest, see manifest builds |
-v, --verbose | Detailed logging |
-s, --silent | Suppress output |
When --output is a URL (e.g., a presigned S3 URL), the CLI bundles to a temp file and uploads via HTTP PUT. This is used by the walkerOS app for remote builds.
For server flows, -o resolves to a directory: dist/{flow.mjs, package.json, node_modules/}. When -o ends in .tar.gz or .tgz, that same directory is packed into a single gzip archive instead (server flows only). For multi-flow --all, each flow gets its own subdirectory: dist/<flowName>/....
Multi-Flow Example
# Bundle development flow
walkeros bundle config.json --flow development
# Bundle production with stats
walkeros bundle config.json --flow production --stats
# Bundle all flows
walkeros bundle config.json --allBuild-time values (config.bundle.env)
A web flow resolves its $env.NAME references when it is bundled and inlines
the values into walker.js. The flow declares those values under
config.bundle.env, so the same config bundles the same way wherever it is
built:
"flows": {
"default": {
"config": {
"platform": "web",
"bundle": {
"env": { "GA4_MEASUREMENT_ID": "G-XXXXXXX" }
}
}
}
}- Values must be literal strings. A reference inside one (
$env.,$var.,$secret.,$flow.,$contract.,$code:) is refused. - A local
walkeros bundlelayersconfig.bundle.envover your shell environment, declared values winning. A hosted build and a manifest build use the declared values alone, so a missing name fails the build instead of reading the build machine's environment. - A
$flow.<sibling>reference resolves against the entry flow'sconfig.bundle.env, not the sibling's own. - Everything declared here ends up in a public bundle. Never put a secret in it; web flows cannot hold secrets.
Server flows ignore config.bundle.env: their $env references are read at
runtime from the process environment of the runtime.
Include folders
The root include field lists folders the bundler copies next to a server bundle, for files the flow reads at runtime (see serving files). Without include, a shared/ folder next to the config is copied when it exists. Including the output directory itself is refused as a circular copy. A server build served from the build cache copies them too, so a clean output directory gets the same files as a fresh build.
include applies to server builds only. A browser bundle is served on its own and cannot read local folders, so a web build copies nothing and logs once:
include is ignored for web builds: a browser bundle cannot read local folders.
For the same reason a manifest build of a web flow accepts include and ignores it; only a server flow's include is refused there.
Package specs
Every package the bundler installs must come from the npm registry: a
version (1.2.3), a range (^1.2.0) or a tag (latest). This applies to
config.bundle.packages, to config.bundle.overrides values, and to the
dependencies those packages pull in. Git, github:, file:, link: and
tarball-URL specs are refused before any download, because fetching them can
run code. An optional transitive dependency with such a spec is skipped with a
warning. Install scripts of downloaded packages do not run. For local
development, use a local package path instead.
Manifest builds (--manifest)
walkeros bundle --manifest <url-or-path> builds from a JSON manifest instead
of a config file. It is the contract a build orchestrator (a CI job, a hosted
build job, your own script) uses to drive the CLI: the manifest names the flow,
the artifacts to build and a presigned PUT URL for each; the CLI builds them,
uploads each one, then uploads a result. A bare --manifest reads the
location from BUILD_MANIFEST_URL. Only --json, --verbose and --silent
combine with it.
| Field | Description |
|---|---|
version | 1 |
toolchain | The exact @walkeros/cli version. Any other version refuses the build |
flowConfig | The flow.json content. Required when any bundle artifact is requested |
flowName | Flow to build in a multi-flow config |
buildEnv | Base values for web $env, beneath config.bundle.env. Never the CLI process's own environment |
artifacts[] | At least one. Each has a unique outputName (a bare file name), a putUrl, and optional contentType and headers. A bundle artifact sets target (cdn, cdn-skeleton, runner, simulate or push); a wrap artifact sets target: "wrap", platform and skeleton |
resultPutUrl | Presigned PUT URL for the result JSON |
The result is { ok, toolchain, artifacts: [{ target, outputName, bytes, sha256 }], error? },
where error carries a code, a message and the outputName being built.
Error codes: INVALID_MANIFEST, TOOLCHAIN_MISMATCH, INVALID_CONFIG,
MISSING_BUILD_ENV, WEB_SECRET_REF, UNSUPPORTED_PACKAGE_SPEC,
LOCAL_PATH_NOT_ALLOWED, BUILD_FAILED, UPLOAD_FAILED.
Unknown top-level fields are ignored, so an orchestrator can keep its own
bookkeeping in the manifest. Artifact entries are strict. A manifest build
refuses every local filesystem input (local step packages, a package path,
bundle.traceInclude, a server flow's include) with LOCAL_PATH_NOT_ALLOWED. The Zod
schemas are exported from @walkeros/cli as BuildManifestSchema and
BuildResultSchema, the codes as BUILD_ERROR_CODES.
Push command
The push command executes your flow with a real event. By default a server flow makes actual API calls to your configured destinations. A web flow runs in a simulated page, so its network requests (fetch, sendBeacon, XMLHttpRequest with @walkeros/cli 4.7.1 or later) are recorded instead of sent, and --json lists them as networkCalls. Scripts, iframes and stylesheets the page loads are still fetched. Use --simulate to mock specific steps for safe testing. It accepts either a config JSON (which gets bundled) or a pre-built bundle.
Use Case
You want to:
- Test event processing with mocked destinations (
--simulate) - Test with real third-party APIs (GA4, Meta, BigQuery, etc.)
- Verify production credentials and endpoints work
- Debug actual API responses and errors
- Perform integration testing before deployment
- Execute a pre-built bundle without rebuilding
Push handles both safe local testing (with --simulate) and real integration testing.
Basic Usage
# With config JSON (auto-bundled)
walkeros push flow.json --event '{"name":"page view","data":{"title":"Home"}}'
# With pre-built bundle
walkeros push dist/flow.mjs --event '{"name":"page view"}'Step-by-Step Guide
1. Create a flow configuration
Create api-flow.json with a destination that makes real HTTP calls:
{
"version": 4,
"flows": {
"default": {
"config": {
"platform": "server",
"bundle": {
"packages": {
"@walkeros/server-destination-api": { "version": "^4" }
}
}
},
"destinations": {
"api": {
"package": "@walkeros/server-destination-api",
"config": {
"settings": {
"url": "https://your-endpoint.com/events",
"method": "POST"
}
}
}
},
"collector": { "run": true }
}
}
}2. Create an event file
Create event.json:
{
"name": "order complete",
"data": {
"id": "ORD-12345",
"total": 299.99,
"currency": "USD"
},
"user": {
"id": "user_abc123"
}
}3. Push the event
walkeros push api-flow.json --event event.json --verbose4. Review the output
📥 Loading event...
📦 Loading flow configuration...
🔨 Bundling flow configuration...
🖥️ Executing in server environment (Node.js)...
Pushing event: order complete
✅ Event pushed successfully
Event ID: 1701234567890-abc12-1
Entity: order
Action: complete
Duration: 1234ms
The event was sent to your real API endpoint!
Options
walkeros push <input> [options]| Option | Description |
|---|---|
-e, --event <source> | Required. Event to push (JSON string, file path, or URL) |
-f, --flow <name> | Flow name (for multi-flow configs) |
-p, --platform <platform> | Platform override (web or server) |
--simulate <step> | Simulate a step (repeatable). Mocks the step's push, captures result. Use source.NAME, transformer.NAME, destination.NAME, or collector.NAME (for example collector.default). A transformer runs through its own before and next; the collector step runs the collector's enrichment and then collector.next, so it returns every copy that would reach the destinations (none when a stop drops the event). |
--ingest <source> | Request context for the simulated step (JSON string, file path, or URL; must be a JSON object), for example {"ip":"203.0.113.7","userAgent":"Mozilla/5.0"}. Applies to transformer.*, collector.* and destination.* simulation only. |
--consent <source> | Collector consent the simulated step starts from (JSON string, file path, or URL; a JSON object of booleans), for example {"functional":true,"marketing":true}. Applies to every simulated step, sources included; rejected for a real push. See consent in simulate. |
--command <name> | The simulated destination runs this collector command (config, consent, user or run) with the event as its data, instead of a push. destination.* simulation only. |
--page-url <url> | Page URL of a simulated web source (absolute http or https URL). source.* simulation of a web flow only. Default: the input trigger's options.url, else http://localhost. |
--mock <step=value> | Mock a step with a specific return value (repeatable). Use destination.NAME=VALUE, a chain member such as destination.NAME.before.TRANSFORMER=VALUE, or a member of the collector chain with collector.next.TRANSFORMER=VALUE. |
--snapshot <source> | JS file to eval before execution. Sets global state (window.dataLayer, process.env, etc.). |
-o, --output <path> | Write result to file |
--json | Output results as JSON. For a real and a simulated push alike, stdout carries only the JSON document; logs go to stderr |
-v, --verbose | Verbose output with debug information |
-s, --silent | Suppress output (for CI/CD) |
Input Types
The CLI auto-detects the input type by attempting to parse as JSON:
- Config JSON - Bundled and executed
- Pre-built bundle (
.js/.mjs) - Executed directly
When using pre-built bundles, platform is detected from file extension:
.mjs→ server (ESM, Node.js).js→ web (IIFE, JSDOM)
Use --platform to override if extension doesn't match intended runtime.
Event Input Formats
The --event parameter accepts three formats:
Inline JSON string:
walkeros push flow.json --event '{"name":"page view","data":{"title":"Home"}}'File path:
walkeros push flow.json --event ./events/order.jsonURL:
walkeros push flow.json --event https://example.com/sample-event.jsonPush modes
| Mode | Flag | API Calls | Use Case |
|---|---|---|---|
| Real | (none) | Real HTTP requests (server), recorded requests (web) | Integration testing, production validation |
| Simulate | --simulate | Mocked (captured) | Safe local testing |
| Mock | --mock | Returns mock value | Controlled testing |
Recommended workflow:
- Use
push --simulatefirst to validate configuration without side effects - Use
push(without flags) to verify real integrations work before deployment
Simulation output
A simulated destination runs with the mock env its package ships (examples.env.push), so the vendor SDK or HTTP call is recorded instead of sent. The output lists, per simulated step, the mapping rule that matched and every recorded vendor call:
walkeros push flow.json -e '{"name":"order complete","data":{"id":"ORD-1","total":42}}' \
--simulate destination.pubsubsuccess: true
destination.pubsub
mapping: none
call PubSub.topic.publishMessage({"data":"{\"name\":\"order complete\",\"data\":{\"id\":\"ORD-1\",\"total\":42},...)
Duration: 44779ms
Each destination step prints mapping: <entity action key> when a rule matched, mapping: none when it pushed the event without a matching rule, or mapping: none (skipped before mapping) when nothing was sent, then one call <fn>(<args>) line per recorded call (arguments as compact JSON, cut at 300 characters) or no calls. When nothing was sent, a line before the calls says why:
pending: waits for consent (require): the destination hasrequireand never started (it names the unmetrequireentries).skipped: consent (requires marketing; granted functional): the destination started, but its consent check skipped the event. It names the consent the destination requires and what was granted (collector consent plus the event's own consent).
Source, transformer and collector steps print their recorded calls, then the events they pass on (event <json> or no events).
A source step also records the walker commands the source issues itself, as elb calls in the source's own call shape: a CMP source's walker consent, the session source's user and session. The collector's own commands (the starting consent, globals and custom of the flow, the final shutdown) and the wiring commands on, hook and destination are not recorded. The Usercentrics source of flow-complete.json, with the in and trigger of its explicitDecision example as uc.json:
walkeros push flow-complete.json --flow web -e uc.json --simulate source.usercentricssuccess: true
source.usercentrics
call elb("walker consent",{"functional":true,"marketing":true})
no events
Duration: 811ms
With --json, the command arrives as { "fn": "elb", "args": ["walker consent", { "functional": true, "marketing": true }], "ts": ... } in the step's calls.
With --json, the same results arrive under simulations, one entry per simulated step, with the full call arguments:
{
"success": true,
"duration": 43222,
"simulations": [
{
"step": "destination",
"name": "pubsub",
"events": [],
"calls": [
{
"fn": "PubSub.topic.publishMessage",
"args": [{ "data": "{\"name\":\"order complete\",...}" }],
"ts": 1790265170276
}
],
"duration": 42841
}
]
}
A destination that sent nothing carries skipped in its entry: { "reason": "pending", "require": ["consent"] }, or { "reason": "consent", "required": { "marketing": true }, "granted": { "functional": true } }.
Arguments are made printable (an Error becomes { name, message }, a Buffer its UTF-8 text, bigint a string, Map and Set arrays, cycles "[Circular]"). Both text and JSON output pass through the same secret scrubbing the CLI logs use, so service account keys, private keys, Authorization headers, access_token parameters and credential fields show as ***. The values of the secrets the flow references (every $secret.NAME that is set in the environment, 6 characters or longer) are masked as they are, also inside JSON strings and URL-encoded in a URL or form body. With --json, a number whose printed form contains a known value (6 characters or longer) becomes the string "***", so the document still parses. The destination itself still receives the real values.
A simulation and a real push of a flow config both route the flow's own logs through the same masking CLI logger (to stderr with --json), so the flow's debug lines appear only with --verbose. The run's own CLI lines (loading, bundling, pushing) mask the known values too. A real push of a prebuilt bundle keeps the bundle's own logger unless --json is set. The runtime masks the fetched secret values in its logs too (see the runtime docs).
What starts in a simulation. A destination simulation starts only its target destination: no source and no other destination initializes, so no other vendor script or SDK client is built. The flow's transformers still start, and a store whose package ships no mock env runs as configured (with --verbose: Stores without a mock env run as configured: ...). A source simulation starts only the simulated source, so a web source never adds the browser source's page view or a CMP decision of its own. The events it captures stop at the collector and reach no destination. A source whose package declares recorded calls (examples.env.simulation, for example the SQS and Pub/Sub pull sources) runs on its mock client, and those calls are listed like a destination's.
No real vendor calls. The guarantee covers destinations. When a package destination's export has no mock env, simulate stops before the flow starts (a step with inline code has no package and runs as given):
No mock env for @my/destination export default: simulate would call the real vendor. Add examples.env.push to the package's dev examples.
A multi-export package (for example @walkeros/server-destination-gcp with destinationBigQuery and destinationPubSub) keys its dev examples by export name, and simulate picks the entry for the step's import. A package version published before that convention is refused for a named export with ...: this package version predates export-keyed examples; use a version with exportExamples or a local path.
Request context with --ingest. Destinations that read the request (client IP, user agent) get it from the ingest context, which an HTTP source fills in a running flow. Pass it with --ingest to see what those destinations send:
walkeros push flow.json -e event.json --simulate destination.meta \
--ingest '{"ip":"203.0.113.7","userAgent":"Mozilla/5.0"}'The ingest reaches the destination's before chain and its push context (for a collector step, collector.next). Runtime metadata (_meta) is always set by the CLI. --ingest is rejected for source.* simulation and for a real push.
Consent in simulate
Three settings decide whether a destination receives an event. They are easy to mix up, and a simulation shows each one separately:
| Setting | Where | What it does | In a simulation |
|---|---|---|---|
require: ["consent"] | Destination config | The destination stays pending (not initialized, events queued) until the collector has any consent state | Starts once --consent is passed. Without it: pending: waits for consent (require) |
consent: { marketing: true } | Destination config | Each event is checked against the collector's consent plus the event's own consent; a denied event is skipped and held in the destination's queue until the consent is granted | Grant it with --consent or in the event's consent. Otherwise: skipped: consent (...) |
consent on the event | The event (-e) | Consent of this one event, counted on top of the collector's consent | Part of the event you pass |
consent in a source's settings | Source config.settings (for example the session source) | The source waits for that consent before it emits | A consent-gated source starts from --consent. Without it, it emits nothing |
--consent sets the collector consent the simulated step starts from. The flow starts with it (it is applied before the flow runs, as a consent command that only the simulated step hears), so a destination with require: ["consent"] starts, and its consent check sees the granted keys. A destination with Consent Mode enabled (como) records the resulting gtag('consent','update',...) call. For a source simulation it is the consent a consent-gated source finds when it starts; this starting consent is never recorded as one of the source's calls. Without --consent the simulation grants nothing. The value is a JSON object of booleans (otherwise: --consent must be a JSON object of booleans), given inline, as a file path or as a URL. --consent is rejected for a real push:
--consent sets the collector's starting consent for a simulation; a real push uses the flow's own consent.
The session source of flow-complete.json waits for functional consent. With the in and trigger of its marketingSession example as session.json and no --consent, it prints no events. With starting consent it starts one session from the ad click (output shortened):
walkeros push flow-complete.json --flow web -e session.json --simulate source.session \
--consent '{"functional":true}'success: true
source.session
call elb("user",{"session":"bligwfsxsbb0kkjl","device":"pixjjoo217w3rxl4"})
call elb("session",{"id":"bligwfsxsbb0kkjl","start":1790334598111,"isNew":true,"count":1,"runs":1,"isStart":true,"storage":true,"referrer":"www.google.com","campaign":"trail","medium":"cpc","source":"google","gclid":"gclid-abc123","clickId":"gclid","platform":"google","marketing":true,"device":"pixjjoo217w3rxl4","updat...)
event {"name":"session start","data":{"id":"bligwfsxsbb0kkjl",...,"gclid":"gclid-abc123","clickId":"gclid","platform":"google","marketing":true,...}}
Duration: 1564ms
The GTM destination of flow-complete.json has require: ["consent"] and consent: { marketing: true }. With the event of its productAdd example, and no consent:
walkeros push flow-complete.json --flow web -e product-add.json --simulate destination.gtmsuccess: true
destination.gtm
mapping: none (skipped before mapping)
pending: waits for consent (require)
no calls
Duration: 646ms
With starting consent, it starts and sends:
walkeros push flow-complete.json --flow web -e product-add.json --simulate destination.gtm \
--consent '{"functional":true,"marketing":true}'success: true
destination.gtm
mapping: product add
call window.gtag("consent","default",{"ad_storage":"denied","ad_user_data":"denied","ad_personalization":"denied","analytics_storage":"denied"})
call window.gtag("consent","update",{"analytics_storage":"granted","ad_storage":"granted","ad_user_data":"granted","ad_personalization":"granted"})
call window.dataLayer.push({"event":"elb product add","consent":{"functional":true,"marketing":true},"globals":{"site":"shop","language":"en","pageGroup":"product"},"user":{"device":"d3v1c3","session":"s3ss10n"},"data":{"id":"SKU-1","name":"Trail Runner","price":129.9,"quantity":1}})
Duration: 968ms
With only functional granted and an event that carries no consent of its own, the destination starts but its consent check skips the event:
success: true
destination.gtm
mapping: none (skipped before mapping)
skipped: consent (requires marketing; granted functional)
no calls
Duration: 710ms
Command examples with --command
A step example with command (config, consent, user or run) runs a collector command instead of pushing an event. --command <name> does the same for a destination simulation: the event you pass is the command's data, and the calls the destination makes in response are recorded. The GA4 destination of flow-complete.json has Consent Mode enabled; its consentUpdate example sends {"functional":true,"marketing":true} as a consent command:
walkeros push flow-complete.json --flow web -e '{"functional":true,"marketing":true}' \
--simulate destination.ga4 --command consentsuccess: true
destination.ga4
mapping: none
call window.gtag("consent","default",{"ad_storage":"denied","ad_user_data":"denied","ad_personalization":"denied","analytics_storage":"denied"})
call window.gtag("config","G-MAINSITE",{})
call window.gtag("consent","update",{"analytics_storage":"granted","ad_storage":"granted","ad_user_data":"granted","ad_personalization":"granted"})
Duration: 618ms
Combined with --consent, the starting consent applies first, then the command runs. --command is rejected for every other step and for a real push: --command applies to destination simulation only.
Web sources and --page-url
A web source runs in a simulated browser page. Its URL is --page-url, else the input trigger's options.url, else http://localhost. Only absolute http and https URLs are accepted (otherwise: --page-url must be an absolute http(s) URL, e.g. https://www.example.com/ (got "...")). The browser source of flow-complete.json, fired by a load trigger:
walkeros push flow-complete.json --flow web -e '{"trigger":{"type":"load"},"content":""}' \
--simulate source.browser --page-url https://shop.example.com/cartsuccess: true
source.browser
event {"name":"page view","data":{"domain":"shop.example.com","title":"","referrer":"","id":"/cart"},"context":{},"globals":{},"trigger":"load","source":{"type":"browser","platform":"web","url":"https://shop.example.com/cart","referrer":""},"id":"80840d79871a5058"}
Duration: 630ms
The browser source's example triggers apply their own options.url as a path: with --page-url, the pageView example keeps the --page-url origin and switches to its path (https://shop.example.com/product/trail-runner). --page-url is rejected for every other step, for a server source and for a real push: --page-url sets the page of a simulated web source; for request context use --ingest.
Simulate versus trace mode. Simulate injects the mock env before the destination's init, so clients built in init (the BigQuery writer, the Pub/Sub client) are recorded. A running flow observed at trace level records calls per push only, so calls through a client built in init do not appear there.
Secrets in simulate. A $secret.NAME reference is read from the environment variable NAME, also in simulate. For a Sheets or GCS store the value must be a valid service-account JSON with a real-format RSA private key: the store signs its token request before the mocked fetch answers it, so a hand-made fake key fails at flow start with a key decoding error. A throwaway key is fine, it is never sent anywhere:
export GCP_SA=$(node -e 'const {privateKey}=require("crypto").generateKeyPairSync("rsa",{modulusLength:2048,privateKeyEncoding:{type:"pkcs8",format:"pem"},publicKeyEncoding:{type:"spki",format:"pem"}});console.log(JSON.stringify({type:"service_account",project_id:"demo",client_email:"sim@demo.iam.gserviceaccount.com",private_key:privateKey}))')JSON Output
For CI/CD pipelines, use --json for machine-readable output:
walkeros push flow.json --event '{"name":"page view"}' --jsonOutput:
{
"success": true,
"event": {
"id": "1701234567890-abc12-1",
"name": "page view",
"entity": "page",
"action": "view"
},
"duration": 1234
}
On error:
{
"success": false,
"error": "Connection refused: https://your-endpoint.com/events",
"duration": 5023
}
For a real and a simulated push alike, stdout carries only this JSON document (logs go to stderr), so it pipes straight into jq. With --simulate, the result also carries simulations (see Simulation output).
Multi-Flow
Push to specific flows:
# Push to staging
walkeros push flow.json --event event.json --flow staging
# Push to production
walkeros push flow.json --event event.json --flow productionSetup command
The setup command runs the optional setup() lifecycle on a single component to provision external resources before traffic flows. Use it for one-time provisioning steps like creating a BigQuery dataset and table, declaring a Pub/Sub topic and subscription, creating SQLite tables, or registering a webhook with a third-party API.
For the conceptual overview of the lifecycle (when to implement setup() in a component, how it relates to the runtime hot path, and the operator separation rationale), see Setup lifecycle.
Use case
Setup is for the operator-time, "do this once before traffic" step that sits between writing config and accepting events:
- A BigQuery destination needs its dataset and table to exist with the right schema before rows can be streamed in.
- A Pub/Sub source needs a subscription against a topic before it can pull messages.
- A SQLite store needs its tables created before reads and writes work.
- A webhook integration needs to register its callback URL with the upstream service.
Setup is explicit only. It is never triggered by push, simulate, deploy, or the long-running runtime. You run it once when provisioning, then again only when external resources need to be re-provisioned (for example, after changing the table schema in config). The runtime never tries to "fix" missing resources on its own.
Basic usage
# Provision the BigQuery dataset and table for the destination named "bigquery"
walkeros setup destination.bigqueryTarget syntax
The target uses the same <kind>.<name> syntax as walkeros push --simulate. Valid kinds are source, destination, and store. Transformers are pure functions and have no setup.
| Target | Resolves to |
|---|---|
destination.bigquery | The destination named bigquery in the resolved flow |
source.events-in | The source named events-in |
store.session | The store named session |
Package resolution
Setup resolves the component's package exactly like walkeros bundle: the version pinned in config.bundle.packages (or inline on the step as "@scope/pkg@x.y.z", with the bundle pin winning on disagreement) is downloaded from the npm registry into a temporary tree and imported from there.
{
"config": {
"platform": "server",
"bundle": {
"packages": {
"@walkeros/server-destination-gcp": { "version": "4.4.0" }
}
}
},
"destinations": {
"bigquery": {
"package": "@walkeros/server-destination-gcp",
"config": { "setup": true }
}
}
}What this means in practice:
- Works from any install. npx, devDependency, or global: no local
npm installof the component package is needed or consulted. - Shared cache with bundle. Exact version pins keep working offline once cached;
latestand ranges re-resolve against the registry. - Local packages via
path. Apathentry inconfig.bundle.packagesis supported for local development; the target must be a built package directory (package.json plus dist output), since setup imports with Node at runtime. - No version drift with exact pins. Setup and bundle resolve through the same pipeline and cache, so an exact version pin guarantees both act on the same package release and provisioning (for example a BigQuery schema) cannot drift from what the bundle writes. Mutable specifications (
latest, ranges,pathtargets) can resolve differently between separate runs, so pin exact versions when drift matters.
Options
walkeros setup <target> [options]| Option | Description |
|---|---|
-c, --config <path> | Flow config file (default: ./flow.json) |
-f, --flow <name> | Flow name for multi-flow configs |
--json | Output as JSON (passed to the package's setup logger) |
-v, --verbose | Verbose output |
-s, --silent | Suppress output |
What "skip" means
The framework narrates one of three reasons when nothing runs and exits successfully:
| Reason | Trigger |
|---|---|
no setup function | The package does not export a setup function on its default export |
config.setup is false | The flow config explicitly opts out with "setup": false |
config.setup is unset | The flow config does not declare setup at all |
In all three cases the exit code is 0. Setup is a single-purpose command, not a tiered status check.
Result output
When the package's setup() returns a non-undefined value, the CLI emits it as JSON on stdout for jq-style scripting. This lets you script around what was provisioned:
walkeros setup destination.bigquery --json | jq .datasetCreatedIf the package returns nothing, no JSON is emitted; you only see the framework narration.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success or skip (any of the three skip reasons above) |
| non-zero | Failure (resolution error, missing default export, package threw) |
IAM note
Setup typically needs higher permissions than runtime push: "create dataset" or "create subscription" vs "write rows" or "pull messages". Operators commonly run setup with a separate service account, or different credentials, than the runtime uses. Plan your IAM accordingly: grant the runtime only what it needs to push, and grant setup the broader provisioning scope.
Examples
# Provision the BigQuery dataset and table for the destination named "bigquery"
# in the default flow file.
walkeros setup destination.bigquery
# Same but pointing to a specific flow in a multi-flow config.
walkeros setup destination.bigquery --flow analytics
# Custom config path.
walkeros setup destination.bigquery --config ./flows/prod.json
# Pipe the structured result to jq.
walkeros setup destination.bigquery --json | jq .datasetCreated
# Provision a Pub/Sub source's subscription.
walkeros setup source.events-inValidate command
The validate command checks the structure and correctness of events, flow configurations, mapping configurations, contracts, or individual flow entries before bundling or deployment.
Use case
Before bundling or deploying your flow, you want to:
- Catch configuration errors early
- Verify event structure follows walkerOS conventions
- Check mapping patterns are valid
- Validate contracts follow the named-entry shape, with entity-action schemas nested under
events - Validate a specific destination, source, or transformer entry against its package's published JSON Schema
- Integrate validation into CI/CD pipelines
Validate gives you fast feedback without the overhead of bundling.
What validate guarantees
- Everything by default.
walkeros validate flow.jsonruns every check on every flow and every step: the schema, references, routes, examples, the rootcontract, the bundler's own preflight and resolver, and each step's settings against its package schema. - Narrowing is explicit.
--flow <name>narrows every per-flow check to that flow; file-level checks (file shape, rootcontract) still run.--path <section.key>checks one entry in every flow that has it, or only in--flow. An unknown flow isFLOW_NOT_FOUND, an unknown entryENTRY_NOT_FOUND(exit 1). - Every skip is reported. A check that could not run is listed in
details.skipped(and in the text output) with a reason and a code. A run with skips never printsAll checks passed. - The result states its scope.
details.scopelists the flows and checks that ran; the text output starts with aScope:line. --strictescalates, never changes the scope. Warnings and skips fail (exit 2;validate()andflow_validatereturnvalid: falsewithstrict: true), and contract example disagreements become errors.- Stable machine output.
--jsonand the MCP toolflow_validatereturn the same shape; every error and warning carries a stablecode. - One verdict everywhere. The CLI,
validate()andflow_validatereturn the same result for the same file, and a valid flow passes the bundle preflight and resolves as the bundler resolves it. One exception: the hosted MCP server has no filesystem, so a step whose package is a localpathis a skipped check there.
Basic usage
# Validate a flow configuration (schema, references, cross-step examples)
walkeros validate flow.json
# Validate an event structure
walkeros validate event.json --type event
# Validate a mapping configuration
walkeros validate mapping.json --type mapping
# Validate a contract
walkeros validate contract.json --type contract
# Validate a specific entry against its package schema
walkeros validate flow.json --path destinations.snowplowValidation types
The --type option accepts four validation types. Default is flow:
| Type | What it validates |
|---|---|
event | Event structure: name field exists, follows "entity action" format with space, valid data types |
flow (default) | Flow.Json config: schema, references, routes, the root contract, the bundle preflight and resolver, each step's settings against its package schema, cross-step example compatibility, and the examples of @walkeros/transformer-validate steps against their linked contracts, plus a PACKAGE_VERSION_MISSING warning for each config.bundle.packages entry without a version or path |
mapping | Mapping rules: event patterns use "entity action" or wildcard format, rule structures are valid |
contract | Contract structure: named entries with optional extend, tagging, description, events, and schema fields. On a flow file it validates the file's contract section (NO_CONTRACT_SECTION if there is none) |
Use --path for entry validation against package schemas (e.g., --path destinations.snowplow).
Step-by-step guide
1. Validate a flow configuration
walkeros validate flow.jsonOutput (valid):
Validating flow...
Scope: flows default; 11 check(s): file:schema, flow:schema, flow:steps, flow:package-versions, flow:structure, flow:routes, flow:examples, flow:contract-examples, flow:mapping-keys, flow:resolve, flow:package-settings
Validation Results:
✓ All checks passed
Summary: 0 error(s), 0 warning(s), 0 skipped
2. Validate with errors
# Create an invalid event
echo '{"name":"pageview"}' > bad-event.json
walkeros validate bad-event.json --type eventOutput:
Validating event...
Scope: 1 check(s): event
Validation Results:
✗ name: Event name must be "entity action" format with space (e.g., "page view")
⚠ consent: No consent object provided
→ Consider adding a consent object for GDPR/privacy compliance
Summary: 1 error(s), 1 warning(s), 0 skipped
3. Validate with warnings
# Event without consent object
echo '{"name":"page view","data":{"title":"Home"}}' > event.json
walkeros validate event.json --type eventOutput:
Validating event...
Scope: 1 check(s): event
Validation Results:
⚠ consent: No consent object provided
→ Consider adding a consent object for GDPR/privacy compliance
✓ All checks passed
Summary: 0 error(s), 1 warning(s), 0 skipped
4. Warnings and skips under --strict
With --strict, warnings and skipped checks fail the run (exit 2), and the verdict line says so:
Validating flow...
Scope: flows server; 11 check(s): file:schema, flow:schema, flow:steps, flow:package-versions, flow:structure, flow:routes, flow:examples, flow:contract-examples, flow:mapping-keys, flow:resolve, flow:package-settings
Validation Results:
⚠ flows.server.destinations.pubsub.setings: Unknown key "setings" on Destination is ignored by the runtime
→ Remove the key, or move it to where it belongs.
- skipped flow:package-settings at flows.server.destinations.pubsub: @walkeros/server-destination-gcp@4.6.1 predates per-export settings schemas; the step imports "destinationPubSub"
✗ Failed under --strict: 1 warning(s) and 1 skipped check(s) count as failures
Summary: 0 error(s), 1 warning(s), 1 skipped
Without --strict the same file exits 0 and ends with No errors in checked scope; 1 check(s) skipped.
Route checks
Flow validation reads every chain field (source.before, source.next, transformer.before, transformer.next, collector.next, destination.before, destination.next) with the same route grammar the runtime uses:
| Check | Level | Example message |
|---|---|---|
| A route names a transformer that does not exist | Error UNKNOWN_ROUTE_TARGET | Unknown transformer "x" in route at collector.next |
Entries after an unconditional stop in a sequence or one | Warning | dead code after stop at ...: entries after an unconditional stop never run |
An array made only of route configs (an implicit one, the first match wins) | Warning | first-match array at ... |
many with no entry, or with a single entry | Warning | empty many at ..., single-entry many at ... |
At runtime an unknown id is logged and skipped, and the chain continues. A stop inside many ends only its own copy, so it never makes its siblings dead code. Repeated steps and a transformer's own next inside an array are valid and not reported.
Reference checks
$flow., $store., $secret. and $contract. resolve only as the whole string value; $var. and $env. also resolve inline. Flow validation warns when a whole-value reference would not resolve:
| Value | Warning |
|---|---|
"$flow.server.url/collect" | does not match the $flow.NAME grammar (...); it would not resolve. To build a longer string, compose with $var or $env. |
"$secret.api_key" | does not match the $secret.NAME grammar (uppercase letters, digits and _); it would not resolve. ... |
"https://x/$store.cache" | uses $store. inline. $store. references resolve only as the whole value; it would ship as literal text. ... |
Values under title, description and $comment, and $code: values, are not checked.
References resolve per flow, the way the bundler resolves them: a $var or $store defined only in another flow does not count. Flow validation resolves every flow with the runtime resolver, so an unknown $var, a missing store, a $flow that does not resolve (unknown flow, or a target without that value) or a reference cycle is an error (UNRESOLVED_REFERENCE, STORE_REFERENCE_NOT_FOUND, FLOW_CYCLE, VAR_CYCLE), because the bundle of that flow would fail the same way. A step reading its own flow's config through $flow is a FLOW_CYCLE. An $env without a default stays a runtime value and is not an error.
Contract checks
When the file has a root contract, the default run validates it exactly as walkeros validate flow.json -t contract does, with paths under contract.. A dangling extend or a circular chain (reported once per cycle) is an error. A validate step whose contract names a $contract.<name> that does not exist is an error (UNRESOLVED_CONTRACT).
Package settings checks
Every step with a package has its resolved config.settings checked against the package's settings schema, at the version the bundle would install: the config.bundle.packages pin, else the inline @version, else latest. A local path package is read from disk.
- Findings are warnings (
ENTRY_SCHEMA, withkeyword) for one minor release; they become errors after that.--strictfails on them now. - Placeholders:
$varis resolved first and the value checked.$env.NAME:defaultis checked using the default, and an error names the default, never the placeholder.$flowresolves statically; an unresolvable$flowis an error. Values known only at runtime ($env.NAMEwithout default,$secret,$store,$code) are not checked and are listed indetails.deferred. - A schema that cannot be fetched is a skip (
SCHEMA_UNAVAILABLE); a package without a settings schema is a skip (NO_SETTINGS_SCHEMA). - Steps that import a named export are checked against that export's schema. The export is picked as the bundler picks it: the step's
import, else the firstimportsentry of itsconfig.bundle.packagespin, else the default export. A package with several exports publishes a settings schema per export (exportSchemasin itswalkerOS.json); a package with one export uses itssettingsschema for a named import too. An export missing fromexportSchemas, a multi-export package version published before per-export schemas, or a package version that does not list its exports, is a skip (NO_SETTINGS_SCHEMA), so--strictfails on it. --offlineskips this check without fetching anything; theScope:line saysoffline: package settings not checked.
Unknown top-level keys on a destination or store are warnings (UNKNOWN_KEY), and a removed package such as @walkeros/store-memory is a warning (DEPRECATED_PACKAGE).
Example checks
Flow validation compares each step's example out with the in of the steps it connects to. An out written as effects ([["elb", event]], [["return", { "event": ... }]], [["return", [...]]]) contributes the events those effects pass on; other effects (message.ack, response, vendor calls, ["return", false]) pass nothing on.
Contracts are checked where the runtime applies them: only in @walkeros/transformer-validate steps. Each validate step's own examples run through that step's contract, mode, format and output settings, and the example out must agree with the verdict. See Validate.
Options
walkeros validate [input] [options]| Option | Description |
|---|---|
-t, --type <type> | Validation type (default: flow). Also: event, mapping, contract |
--path <path> | Check one entry against its package schema in every flow that has it, or only in --flow (e.g., destinations.snowplow, stores.cache) |
-f, --flow <name> | Narrow every per-flow check to this flow; file-level checks still run |
-o, --output <path> | Write result to file |
--strict | Warnings and skipped checks fail (exit code 2); contract example disagreements become errors |
--offline | Do not fetch package schemas; step settings are not checked, and the scope says so |
--json | Output results as JSON |
-v, --verbose | Show detailed validation information |
-s, --silent | Suppress banner output |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Valid (no errors). Skips alone do not change it |
| 1 | Validation errors found |
| 2 | Warnings or skipped checks (with --strict) |
| 3 | Could not run: input error (file not found, invalid JSON) or an option that does not apply (OPTION_NOT_APPLICABLE, e.g. --path with -t event) |
CI/CD integration
Use --json and exit codes for automated pipelines:
# Fail on any error
walkeros validate flow.json --json || exit 1
# Fail on warnings too (strict mode)
walkeros validate flow.json --strict --json || exit 1JSON output format (details.context, the IntelliSense context, omitted):
{
"valid": true,
"type": "flow",
"errors": [],
"warnings": [
{
"path": "flows.server.destinations.pubsub.setings",
"message": "Unknown key \"setings\" on Destination is ignored by the runtime",
"suggestion": "Remove the key, or move it to where it belongs.",
"code": "UNKNOWN_KEY"
}
],
"details": {
"flowNames": ["server"],
"flowCount": 1,
"scope": {
"flows": ["server"],
"checks": ["file:schema", "flow:schema", "flow:steps", "flow:package-versions", "flow:structure", "flow:routes", "flow:examples", "flow:contract-examples", "flow:mapping-keys", "flow:resolve", "flow:package-settings"]
},
"skipped": [
{
"path": "flows.server.destinations.pubsub",
"check": "flow:package-settings",
"reason": "@walkeros/server-destination-gcp@4.6.1 predates per-export settings schemas; the step imports \"destinationPubSub\"",
"code": "NO_SETTINGS_SCHEMA"
}
],
"deferred": []
}
}
| Field | Meaning |
|---|---|
errors[], warnings[] | { path, message, code, keyword?, value?, suggestion? }. Paths inside a flow start with flows.<flow>.; keyword is the JSON Schema keyword of an ENTRY_SCHEMA finding |
details.scope | { flows, entry?, checks, offline? }: what the run covered. With --path, entry is { section?, key, flows, searchedFlows } |
details.skipped[] | { path, check, reason, code }: checks in scope that did not run |
details.deferred[] | { path, reference }: values known only at runtime, not schema-checked |
details.packages[] | { path, package, version? }: the package version each step was checked against |
Codes group as follows (the full list is exported as VALIDATE_CODES from @walkeros/cli):
| Group | Codes |
|---|---|
| Scope and options | FLOW_NOT_FOUND, ENTRY_NOT_FOUND, AMBIGUOUS_ENTRY, OPTION_NOT_APPLICABLE |
| Structure and resolution | SCHEMA_VALIDATION, INVALID_COMPONENT_NAME, INVALID_REFERENCE, STORE_REFERENCE_NOT_FOUND, STATE_STORE_UNKNOWN, STATE_STORE_FILE, UNRESOLVED_REFERENCE, FLOW_CYCLE, VAR_CYCLE, UNKNOWN_KEY, DEPRECATED_PACKAGE, TRANSFORMER_MAPPING_NO_OP (a warning: a transformer mapping field that does nothing there) |
| Settings | ENTRY_SCHEMA |
| Contracts | NO_CONTRACT_SECTION, INVALID_EXTENDS, CIRCULAR_EXTENDS, UNRESOLVED_CONTRACT |
| Skips | GATED_BY_ERRORS, CONTRACT_NOT_STATIC, NO_PACKAGE, NO_SETTINGS_SCHEMA, SCHEMA_UNAVAILABLE |
Multi-flow validation
Without --flow, validate checks every flow. --flow narrows every per-flow check to one flow; file-level checks (the file shape and the root contract) still run and can still fail the run:
# Per-flow checks for the "production" flow, plus the file-level checks
walkeros validate flow.json --flow productionIf the flow doesn't exist (FLOW_NOT_FOUND, exit 1):
Validation Results:
✗ flows: Flow "production" not found. Available: default, staging
Summary: 1 error(s), 0 warning(s), 0 skipped
Event validation details
Event validation checks (see event.ts):
- Name field exists - Required field
- Name is non-empty - Cannot be empty string
- Entity-action format - Must contain space (e.g.,
"page view"not"pageview") - Schema validation - Data types match expected structure
- Best practices - Warns if consent object is missing
# Valid event
walkeros validate '{"name":"product view","data":{"id":"P123"}}' --type event
# Invalid: missing space in name
walkeros validate '{"name":"productview"}' --type eventMapping validation details
Mapping validation checks (see mapping.ts):
- Object structure - Must be an object with event patterns as keys
- Event patterns - Must be
"entity action"format or contain wildcard (*) - Rule structure - Each rule must be an object or array of objects
- Catch-all position - Warns if
*is not the last pattern
# Valid mapping
walkeros validate '{
"page view": {"name": "pageview"},
"*": {"name": "generic_event"}
}' --type mappingContract validation details
Contract validation checks (see contract.ts):
- Root structure - Must be an object of named contract entries, not an array or primitive
- Named-entry shape enforced - A flat entity-action map, or a
$-prefixed root key, is rejected as the flat shape - Unknown keys - A key on a contract entry outside
extend,tagging,description,events,schemais a warning, not an error tagging- If present, must be a numberschema- If present, must be a JSON Schema objectextend- Must reference an existing contract entry; circular chains are rejected- Entity and action keys - Cannot be empty strings
- Event entries - Each
events.<entity>.<action>value must be a JSON Schema object
# Valid contract
walkeros validate '{
"web": {
"tagging": 1,
"events": {
"page": {
"view": {
"properties": {
"title": { "type": "string" }
}
}
}
}
}
}' --type contractEntry validation (--path)
Entry validation checks one destination, source, transformer or store against its package's settings schema, the same check the default run applies to every step (see Package settings checks). Findings are errors here, since you asked for this entry.
# Validate a specific destination entry
walkeros validate flow.json --path destinations.snowplow
# Validate a specific source entry
walkeros validate flow.json --path sources.browser
# A store entry, only in the "server" flow
walkeros validate flow.json --path stores.cache --flow server
# Short form (works if the key is unique across sections)
walkeros validate flow.json --path snowplowThe entry validator:
- Finds the entry in every flow that has it (or only in
--flow);details.scope.entrylists the matching and the searched flows - Resolves each of those flows as the bundler does (a flow that does not resolve is an error, and its entry check a skip)
- Loads the package's settings schema at the pinned version (from the CDN, or from disk for a local
path) - Checks the resolved
config.settingsagainst it, following the placeholder rule;--pathruns only this check (details.scope.checksis["entry:settings"])
Auth commands
The auth command group manages authentication with the walkerOS app. Authentication is required for app commands (projects, flows, deploy).
Login
Log in to walkerOS through the RFC 8628 device authorization grant. The CLI asks the app for a one-time code, opens your browser on the approval page, and polls until you approve. No password reaches the CLI and no local callback server is opened, so this works the same over SSH, inside Docker and in a cloud IDE.
walkeros auth loginOutput:
! Your one-time code: ABCD-1234
Authorize here: https://app.walkeros.io/oauth/device?user_code=ABCD-1234
Opening browser...
Waiting for authorization... (press Ctrl+C to cancel)
✓ Logged in as user@example.com
Session stored in ~/.config/walkeros/config.json
The session is stored in ~/.config/walkeros/config.json with 0600 permissions: a short-lived access token, its expiry, and a refresh token the CLI rotates on its own, so a login lasts until you log out. Several walkerOS processes can share one config file, so a refresh runs under a lock beside it (config.json.lock) and exactly one of them spends the single-use refresh token.
The connection appears in the app under Account, Connected apps, where disconnecting it stops the CLI on its next call.
For a machine with nobody to approve anything, set WALKEROS_TOKEN to an automation token instead (Account, then Automation tokens). It is used as-is and never refreshed.
| Option | Description |
|---|---|
--url <url> | Custom app URL (default: https://app.walkeros.io) |
--json | Output as JSON |
-v, --verbose | Verbose output |
-s, --silent | Suppress output |
Logout
Revoke the stored refresh token, then remove the credentials from disk. Revocation runs first, because deleting the file alone would leave a credential alive on the server that nothing can reach to retire; it is best effort, so a logout with no network still clears the machine.
walkeros auth logout| Option | Description |
|---|---|
--json | Output as JSON |
-v, --verbose | Verbose output |
-s, --silent | Suppress output |
Whoami
Show the current authenticated user's identity, including email, user ID, and project ID (if the token is project-scoped).
walkeros auth whoamiOutput:
user@example.com
User: usr_abc123
Project: proj_def456
| Option | Description |
|---|---|
-o, --output <path> | Write result to file |
--json | Output as JSON |
-v, --verbose | Verbose output |
-s, --silent | Suppress output |
Token resolution
The CLI resolves a credential in this order, first match wins:
WALKEROS_TOKENenvironment variable. Used as-is, never refreshed.- A static
tokenin the config file, written by an older CLI. Honored until it expires, with a one-line notice pointing atwalkeros auth login. - The OAuth session in the config file, written by
auth login. Refreshed automatically when the access token is close to expiry. - Nothing, and app commands report "Not authenticated. Run
walkeros auth loginfirst."
A refresh that cannot reach the app is reported as such and leaves the saved session in place, so the next command tries again rather than sending you back to the browser.
Projects commands
The projects command group manages walkerOS app projects. All commands require authentication (see Auth commands).
List projects
walkeros projects list| Option | Description |
|---|---|
--cursor <c> | Resume from a pagination cursor returned by a previous list call |
--limit <n> | Maximum number of projects to return |
Get project details
# Get by explicit ID
walkeros projects get proj_abc123
# Uses WALKEROS_PROJECT_ID if omitted
walkeros projects getCreate a project
walkeros projects create "My Analytics Project"Update a project
walkeros projects update proj_abc123 --name "New Name"Delete a project
walkeros projects delete proj_abc123Common options
All projects subcommands support:
| Option | Description |
|---|---|
-o, --output <path> | Write result to file |
--json | Output as JSON |
-v, --verbose | Verbose output |
-s, --silent | Suppress output |
Flows commands
The flows command group manages flow configurations within a project. All commands require authentication and a project context (either --project or WALKEROS_PROJECT_ID).
List flows
walkeros flows list| Option | Description |
|---|---|
--project <id> | Project ID (defaults to WALKEROS_PROJECT_ID) |
--cursor <c> | Resume from a pagination cursor returned by a previous list call |
--limit <n> | Maximum number of flows to return |
--sort <field> | Sort by: name, updated_at, created_at |
--order <dir> | Sort order: asc, desc |
--include-deleted | Include soft-deleted flows |
Get a flow
Retrieves a flow with its full Flow.Json content.
walkeros flows get cfg_abc123| Option | Description |
|---|---|
--project <id> | Project ID (defaults to WALKEROS_PROJECT_ID) |
Create a flow
# With inline JSON
walkeros flows create "My Flow" --content '{"version":4,"flows":{"default":{"config":{"platform":"server"}}}}'
# From a file
walkeros flows create "My Flow" --content ./flow.json
# Via stdin
cat flow.json | walkeros flows create "My Flow"| Option | Description |
|---|---|
--project <id> | Project ID (defaults to WALKEROS_PROJECT_ID) |
-c, --content <content> | Flow.Json as JSON string, file path or URL |
Update a flow
# Update name only
walkeros flows update cfg_abc123 --name "Updated Flow"
# Update content
walkeros flows update cfg_abc123 --content '{"version":4,"flows":{"default":{"config":{"platform":"server"}}}}'
# Update content from a file
walkeros flows update cfg_abc123 --content ./flow.json| Option | Description |
|---|---|
--project <id> | Project ID (defaults to WALKEROS_PROJECT_ID) |
--name <name> | New flow name |
-c, --content <content> | New Flow.Json as JSON string, file path or URL |
Delete a flow
Soft-deletes a flow configuration.
walkeros flows delete cfg_abc123Duplicate a flow
Creates a copy of an existing flow configuration.
walkeros flows duplicate cfg_abc123 --name "Flow Copy"| Option | Description |
|---|---|
--project <id> | Project ID (defaults to WALKEROS_PROJECT_ID) |
--name <name> | Name for the copy (defaults to "Copy of ...") |
Common options
All flows subcommands support:
| Option | Description |
|---|---|
-o, --output <path> | Write result to file |
--json | Output as JSON |
-v, --verbose | Verbose output |
-s, --silent | Suppress output |
Deploy commands
The deploy command group handles deploying flows through the walkerOS app or managing self-hosted deployments with heartbeat registration. All commands require authentication.
deploy create
Create a new deployment. The CLI infers the deployment type (web or server) from the flow configuration.
# From a local config file
walkeros deploy create flow.json
# From a remote flow ID
walkeros deploy create cfg_abc123
# With a specific flow name (multi-config flows)
walkeros deploy create flow.json --flow productionOn success, the CLI displays the deployment ID, slug, and type. It also shows example commands for running the deployment locally or via Docker. The create response does not include a deploy token: deploy tokens are minted separately, so the Docker instructions tell you to create one in the app (Settings, Self-hosted deploy token) and set it as WALKEROS_DEPLOY_TOKEN.
| Option | Description |
|---|---|
--label <string> | Human-readable label for the deployment |
-f, --flow <name> | Flow name for multi-flow configs |
--project <id> | Project ID (defaults to WALKEROS_PROJECT_ID) |
-o, --output <path> | Write result to file |
--json | Output as JSON |
-v, --verbose | Verbose output |
-s, --silent | Suppress output |
deploy start
Deploy a remote flow through the walkerOS app. Auto-detects whether to use web (script hosting) or server (container) deployment based on the flow content. Streams deployment progress via SSE.
# Deploy a flow
walkeros deploy start cfg_abc123
# Deploy a specific config in a multi-config flow
walkeros deploy start cfg_abc123 --flow production
# Deploy without waiting for completion
walkeros deploy start cfg_abc123 --no-waitOutput (web deployment):
Building bundle...
Publishing to web...
✓ Published: https://cdn.walkeros.io/proj_xxx/walker.js
Output (server deployment):
Building bundle...
Deploying container...
Starting container...
✓ Active: https://collect-abc123.walkeros.io
| Option | Description |
|---|---|
--project <id> | Project ID (defaults to WALKEROS_PROJECT_ID) |
-f, --flow <name> | Flow name for multi-config flows |
--no-wait | Return immediately after triggering (do not stream progress) |
--timeout <seconds> | Override the client-side wait budget (defaults to 12 minutes, which covers the server's full deploy budget) |
-o, --output <path> | Write result to file |
--json | Output as JSON |
-v, --verbose | Verbose output |
-s, --silent | Suppress output |
By default deploy start streams progress until the deploy completes. The
client-side wait budget defaults to 12 minutes so a slow but healthy long
deploy is never aborted early and misread as a failure in CI; --timeout
overrides it. Each invocation sends a fresh idempotency key, so retrying after
a failed attempt starts a new deploy instead of replaying the previous result.
When a deploy fails, the CLI prints a stable, machine-readable error.code
alongside the message so CI can branch on the code without matching prose. A
rate-limited trigger (429) is reported as retryable with the Retry-After
hint; under --wait the CLI honors one bounded retry automatically.
deploy list
List all deployments in a project.
walkeros deploy list
# Filter by type
walkeros deploy list --type server
# Filter by status
walkeros deploy list --status active| Option | Description |
|---|---|
--project <id> | Project ID (defaults to WALKEROS_PROJECT_ID) |
--cursor <c> | Resume from a pagination cursor returned by a previous list call |
--limit <n> | Maximum number of deployments to return |
--type <type> | Filter by type: web, server |
--status <status> | Filter by status |
deploy status
Get deployment details by ID or slug.
walkeros deploy status dep_abc123
# Or by slug
walkeros deploy status my-collector| Option | Description |
|---|---|
--project <id> | Project ID |
deploy delete
Delete a deployment by ID or slug.
walkeros deploy delete dep_abc123| Option | Description |
|---|---|
--project <id> | Project ID |
--json | Output as JSON |
Common options
All deploy subcommands support:
| Option | Description |
|---|---|
-o, --output <path> | Write result to file |
--json | Output as JSON |
-v, --verbose | Verbose output |
-s, --silent | Suppress output |
Previews command
The previews command group manages preview bundles: short-lived flow bundles
used to test configuration changes on a real production site before deploying.
Creating a preview mints an app-signed, origin-bound activation grant; opening
?elbPreview={grant} on your site verifies the grant locally, with no network
round trip, and swaps in the preview bundle for that browser for the life of
the preview session. Activation links minted for an Observe session
additionally carry an elbPreviewSession companion grant, which the previewed
page uses to forward server-bound events to that session's isolated container.
All commands require authentication and a project (set via WALKEROS_PROJECT_ID
or --project).
How activation works
A managed web bundle that supports preview activation is produced by the wrap
step's preview option, which bakes in a public key keyring, the issuing
environment, and an opaque per-project binding (never a project ID or secret).
The emitted bundle imports browserSwapActivator from @walkeros/core to
verify an activation grant and swap in the preview artifact. preview is the
current wrap option for this; it replaces the earlier previewOrigin /
previewScope options. A companion previewGrantTargets option exists for the
preview artifact itself: it forwards the stored grant as an
X-Walkeros-Preview header to named server-bound destinations, so a server
flow can be previewed too. preview and previewGrantTargets are mutually
exclusive on a single wrap invocation: a host bundle activates a preview, a
preview artifact injects its grant, not both.
previews list
List all previews for a flow.
walkeros previews list <flowId> [--project <projectId>]| Option | Description |
|---|---|
--project <id> | Project ID (overrides default) |
previews get
Get details for a single preview.
walkeros previews get <flowId> <previewId> [--project <projectId>]| Option | Description |
|---|---|
--project <id> | Project ID (overrides default) |
previews create
Create a preview bundle for a specific flow settings entry.
walkeros previews create <flowId> [options]| Option | Description |
|---|---|
-f, --flow <name> | Flow settings name (resolved to an ID) |
-s, --settings-id <id> | Flow settings ID (alternative to --flow) |
-u, --url <siteUrl> | Your site URL; prints a ready-to-open activation URL on stdout |
--project <id> | Project ID (overrides default) |
Without --url, the CLI prints the activation fragment (?elbPreview=...) to
stdout: append it to any URL on your site to activate preview mode. With
--url, it prints an activation URL for your site on stdout, plus a
deactivation URL on stderr. Most modern terminals make the URL clickable.
You must provide either --flow or --settings-id to pick which flow settings
the preview should bundle.
previews delete
Delete a preview. Removes both the DB row and the S3 bundle immediately; the
production walker on visitors' browsers self-heals on the next page load by
clearing the stored elbPreview grant and loading the production flow.
walkeros previews delete <flowId> <previewId> [options]| Option | Description |
|---|---|
-y, --yes | Skip confirmation (required to run non-interactively) |
--project <id> | Project ID (overrides default) |
Example
# Create a preview for the `demo` flow settings with a ready-to-open URL
walkeros previews create flow_abc123 \
--flow demo \
--url https://example.com
# List previews for a flow
walkeros previews list flow_abc123
# Delete when you're done
walkeros previews delete flow_abc123 prv_xyz456 --yesObserve command
The observe command group opens an Observe session: a time-boxed window on one
flow that runtimes attach to as arms. A preview arm streams from a browser, a
container arm runs server-side, and both feed one shared journeys feed. See
Observe for how a flow connects to an observer
in the first place.
Requires authentication and a project (set via WALKEROS_PROJECT_ID or
--project).
observe start
Mint an Observe session for a flow and print the attach info for each arm.
walkeros observe start <flowId> [options]| Option | Description |
|---|---|
-f, --flow <name> | Flow settings name, which also picks the arms (auto-resolved when the flow has exactly one) |
--project <id> | Project ID (overrides default) |
--level <level> | Observation detail level: off, standard, or trace |
--replace | Replace an existing active session for this flow |
--no-wait | Do not wait for the session to settle |
--timeout <seconds> | Seconds to wait for the session to settle (default: 300) |
--json | Output the session as JSON |
--level steers the provisioned container arm; the web arm's observation detail
does not follow it.
The observed settings picks the arms: a web settings attaches the preview arm, plus the container arm of any server flow it references; a server settings attaches the container arm alone.
A settled session renders both arms. An arm this session did not attach renders
as Web: no web part or Server: no server part in place of its block, so the
output below is the both-arms case:
Observe session ses_abc123 (live)
Web
Bundle URL: https://cdn.test/preview/proj_test/walker.k9x2m4p7abcd.js
Credential: obsw_pb_x.ses_abc123.sec_1
Activate: https://shop.example/?elbPreview=header.payload.signature&elbObserve=obsw_pb_x.ses_abc123.sec_1
Server
Flow: server
Endpoint: https://obs-ses-abc123.containers.test
Env:
WALKEROS_OBSERVER_URL=https://observer.test
WALKEROS_DEPLOYMENT_ID=ses_abc123
WALKEROS_INGEST_TOKEN=srv_ingest_tok
Expires: <ISO timestamp>
Records: 42
The session lives server-side; closing this terminal does not end it.
Sessions idle out server-side when no tab or traffic keeps them alive.Open the Activate: URL on your site to attach the web arm: the link carries
the session credential, so the page starts posting records without any further
setup. Export the three WALKEROS_* variables into a server runtime to attach a
runtime the app did not provision; both arms then feed the same journeys feed.
That block goes to stderr. Stdout carries the live server endpoint alone, so it pipes cleanly:
ENDPOINT=$(walkeros observe start flow_abc123 --flow web)--json prints the whole session to stdout instead, for scripts that need the
credential or the expiry.
Exit codes
| Code | Meaning |
|---|---|
0 | Session settled (or --no-wait returned the mint immediately) |
1 | Session failed to arm, or --timeout elapsed while it was still arming |
A timeout is not a teardown: the session keeps arming server-side, and it is the
app or a re-run of observe start that reports where it landed.
Running a built flow
The CLI builds flows; it does not start them. A built server flow runs with
runneros, the runtime in the separate @walkeros/runner package and the
walkeros/flow image:
walkeros bundle flow.json -o dist/
npx --package=@walkeros/runner runneros start dist/flow.mjsrunneros start accepts the flow.mjs entry of a built directory, a
.tar.gz/.tgz archive from walkeros bundle -o flow.tar.gz, or an http(s)
URL to either. It has no bundler: a local flow config is refused, and one fetched by URL fails to import. See the
runtime for its options, environment variables, health
checks and the Docker image.
Complete example: Web → Server flow
This example demonstrates a complete analytics pipeline:
- Browser events captured by web flow
- Sent to server collection endpoint
- Logged to console (swap for BigQuery in production)
1. Create Server Collection Flow
Create server-collect.json:
{
"version": 4,
"flows": {
"default": {
"config": {
"platform": "server",
"bundle": {
"packages": {
"@walkeros/server-source-express": { "version": "^4" },
"@walkeros/destination-demo": { "version": "^4" }
}
}
},
"sources": {
"http": {
"package": "@walkeros/server-source-express",
"config": {
"settings": { "paths": ["/collect"], "cors": true }
}
}
},
"destinations": {
"console": {
"package": "@walkeros/destination-demo",
"config": {
"settings": { "name": "Server Logger" }
}
}
},
"collector": { "run": true }
}
}
}2. Create Web Tracking Flow
Create web-track.json:
{
"version": 4,
"flows": {
"default": {
"config": {
"platform": "web",
"bundle": {
"packages": {
"@walkeros/web-source-browser": { "version": "^4" },
"@walkeros/web-destination-api": { "version": "^4" }
}
}
},
"sources": {
"browser": {
"package": "@walkeros/web-source-browser",
"config": {
"settings": { "pageview": true, "elb": "track" }
}
}
},
"destinations": {
"api": {
"package": "@walkeros/web-destination-api",
"config": {
"settings": {
"url": "http://localhost:8080/collect",
"method": "POST"
}
}
}
},
"collector": { "run": true }
}
}
}3. Start Collection Server
Terminal 1, build the server flow and start it with the runtime:
walkeros bundle server-collect.json -o dist/server/
npx --package=@walkeros/runner runneros start dist/server/flow.mjs4. Serve the Web Bundle
Terminal 2, build the web flow and serve walker.js with any static file server:
walkeros bundle web-track.json -o dist/web/
npx serve dist/web -l 30005. Test in Browser
Create demo.html:
<!DOCTYPE html>
<html>
<head>
<script src="http://localhost:3000/walker.js"></script>
</head>
<body>
<div data-elb="promotion" data-elb-promotion="name:Get Started" data-elbaction="load:view">
<h1>Welcome to walkerOS</h1>
<button data-elbaction="click:cta">Start Tracking</button>
</div>
<script>
// Manual tracking, once the page has loaded and track() exists
window.addEventListener('load', function () {
track('custom event', { action: 'demo', value: 42 });
});
</script>
</body>
</html>Open it in the browser and click the button. Terminal 1 logs promotion view,
page view and custom event on load, and promotion cta on the click:
[Server Logger] promotion view
[Server Logger] page view
[Server Logger] custom event
[Server Logger] promotion cta
Cache command
The cache command manages the CLI's package and build caches.
Use Case
The CLI caches downloaded npm packages and compiled builds to speed up repeated operations. You may need to:
- Clear stale cached packages when debugging version issues
- Free up disk space by removing old cached builds
- View cache statistics to understand cache usage
How Caching Works
The caches live under cache/ in the system temp directory (os.tmpdir(), which reads TMPDIR on macOS and Linux). CLI output prints that directory as $TMPDIR; on Linux TMPDIR is often unset in a shell, so read a printed $TMPDIR/... path as the system temp dir (usually /tmp).
Package Cache ($TMPDIR/cache/packages/):
- Mutable versions (
latest,^,~) are re-checked daily - Exact versions (
0.4.1) are cached indefinitely - Saves network time on repeated builds
- An entry is written to a temporary sibling and renamed into place once it is complete, so an interrupted write never leaves a partial package behind. A failed write logs a warning and the build continues without caching that package:
Package cache write failed for <name>@<version>: <message>. The build continues without caching it.
- Entries written by an earlier CLI version carry no completion marker and count as incomplete, so each cached package is downloaded once more after the upgrade.
Build Cache ($TMPDIR/cache/builds/):
- Caches compiled bundles based on flow.json content + date
- Identical configs reuse cached builds within the same day
- Dramatically speeds up repeated builds (~100x faster)
- Keys cover the config, the resolved package versions, the built contents of local packages (
path) and the CLI version, so a rebuilt local package or a CLI upgrade builds fresh
Temporary files. Each run works in its own directory, $TMPDIR/walkeros/<kind>/<6 characters> (kind is push, build, bundle, wrap, archive or setup), for example Output: $TMPDIR/walkeros/push/d6WHM6/flow.mjs (1266.1 KB, 0.6s), and removes it afterwards. The short names keep these paths readable in masked logs.
Basic Usage
# View cache statistics
walkeros cache info
# Clear all caches
walkeros cache clearCommands
View cache info:
walkeros cache infoOutput:
Cache directory: $TMPDIR/cache
Cached packages: 12
Cached builds: 5
Cached code: 5
Leftovers of interrupted cache writes are not counted.
Clear all caches:
walkeros cache clearClearing also removes the leftovers of interrupted cache writes.
Clear only package cache:
walkeros cache clear --packagesClear only build cache:
walkeros cache clear --buildsThis removes cached builds and the compiled code they are made from.
Bypassing Cache
To skip the cache for a single build operation:
walkeros bundle flow.json --no-cacheThis downloads fresh packages and rebuilds without using or updating the cache.
Options
| Option | Description |
|---|---|
--packages | Clear only the package cache |
--builds | Clear only the build cache |
Global options
These options work with all commands:
walkeros [command] [options]| Option | Description |
|---|---|
--verbose | Show detailed logs |
--silent | Suppress output |
--json | Output as JSON (for CI/CD) |
--help | Show help for command |
--version | Show CLI version |
Environment variables
| Variable | Purpose |
|---|---|
WALKEROS_TOKEN | Automation token (wos_pat_...) for app commands, from Account then Automation tokens. Overrides the auth login session |
WALKEROS_PROJECT_ID | Default project ID for projects, flows, and deploy commands |
WALKEROS_APP_URL | Base URL override (default: https://app.walkeros.io) |
WALKEROS_DEPLOY_TOKEN | Deploy token for container heartbeat authentication |
WALKEROS_OBSERVE_LEVEL | Baseline telemetry level for runneros start (off, standard, or trace). Invalid values are warned about and ignored. |
BUILD_MANIFEST_URL | Manifest location read by a bare walkeros bundle --manifest |
BUNDLER_STRICT_RANGES | Set to 0 to bypass strict range validation when the npm registry is unreachable during a build |
The runtime reads its own variables, see the runtime environment variables.
Diagnostics command
walkeros diagnostics reports the CLI version, the app URL and where it comes
from (env, config or default), the app's public health route, and the API
contract verdict against that app. It sends no credential, so it works logged
out, and it exits 0 whatever the verdict.
walkeros diagnostics
walkeros diagnostics --jsoncli 4.8.0
app https://app.walkeros.io (default)
health reachable, status ok, app 3f2a91c
contract: in-sync (server 4.7.0+5e6f7a8b, client 4.7.0+1a2b3c4d, 55 operations)
--json prints the same report as one object, { cli, appUrl, app, contract },
where contract carries the verdict with every operation it names. The MCP
diagnostics tool reports the same verdict in its contract block.
API version compatibility
The app labels its API contract <floor>+<hash>, for example 4.7.0+1a2b3c4d.
The floor is the lowest client release the contract works with; the hash names
the exact document. Each CLI and MCP release carries the operations it calls
and their shape, and compares them one by one with the live app's
/api/openapi.json. Operations no walkerOS client calls never count, and
neither do the documentation keywords description, summary, title,
example, examples and externalDocs or an operation's tags. Two labels
with the same floor can therefore differ in their hash while the verdict is
in-sync.
| Verdict | Line | Means |
|---|---|---|
in-sync | contract: in-sync (server 4.7.0+5e6f7a8b, client 4.7.0+1a2b3c4d, 55 operations) | Every operation this client calls has the shape it was built with. |
changed | contract: changed (server 4.7.0+9f8e7d6c, client 4.7.0+1a2b3c4d); changed: GET /api/projects/{projectId}/frames | Operations this client calls differ from the server's, and the server's <floor>+<hash> label names a floor not below the client's, usually the same one. When the server's floor is higher, the server is the newer deploy: upgrade the client. With the same floor a hash has no order, so the server may be a newer or an older deploy. |
server-older | contract: server-older (server 4.6.1, client 4.7.0+1a2b3c4d); not offered (1): GET /api/projects/{projectId}/frames (mcp frame_manage) | The server lacks operations this client calls, listed with the commands and tools that call them, or changed some and names a lower floor or none. A label without +<hash>, such as 4.7.1, predates floors. |
client-outdated | contract: client-outdated (https://app.walkeros.io requires @walkeros/cli >= 4.9.0; this is 4.8.0) | The server's minSupportedClient is above this client's version. |
unknown | contract: unknown (GET https://app.walkeros.io/api/health: timeout after 5000 ms) | The health route or the OpenAPI document could not be read. |
in-process | contract: in-process (server 4.7.0+1a2b3c4d) | The MCP tool runs inside the app it reports (the hosted door). |
Official clients (CLI, MCP) send three headers on every request to the walkerOS app so the server can tell them apart and enforce minimum versions per endpoint:
User-Agent: e.g.walkeros-cli/1.4.0 (node/18.19.0; linux)X-WalkerOS-Client:cliormcpX-WalkerOS-Client-Version: the installed package version
If you call an endpoint that has been updated in a way that requires a newer
client, the app responds with 426 Upgrade Required. The CLI prints the
required minimum version and the upgrade command, then exits with code 2:
Error: This walkerOS app endpoint requires @walkeros/cli >= 1.5.0
(you have 1.4.0). Upgrade with: npm install -g @walkeros/cli@latest
See https://walkeros.io/docs/upgrading for details.
The MCP server surfaces the same information in tool error responses so AI assistants can tell the user to upgrade. See Upgrading for the full version-negotiation rules.
CI/CD integration
GitHub Actions
name: Build Flows
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install CLI
run: npm install -g @walkeros/cli
- name: Bundle flows
run: walkeros bundle flow.json --all --stats --json > stats.json
- name: Upload bundles
uses: actions/upload-artifact@v3
with:
name: flows
path: dist/Docker Build
The canonical multi-stage Dockerfile installs the CLI in the build stage, bundles the flow into a dist/ directory, then copies the directory into the runtime image. You do not need to declare step packages in package.json; pacote installs them transparently from flow.json. The runtime stage has no npm and no bundler.
ARG WALKEROS_VERSION
FROM node:24-alpine AS builder
ARG WALKEROS_VERSION
WORKDIR /build
RUN npm init -y && npm install --save-dev @walkeros/cli@${WALKEROS_VERSION}
COPY flow.json ./
RUN npx walkeros bundle flow.json -o dist/
FROM walkeros/flow:${WALKEROS_VERSION}
COPY --from=builder /build/dist/ /app/flow/Build it with docker build --build-arg WALKEROS_VERSION=<version> ., using a version whose walkeros/flow image ships runneros. The image supplies BUNDLE=/app/flow/flow.mjs, PORT=8080 and the runneros start command. See the runtime.
Troubleshooting
Package Download Issues
If packages fail to download:
# Clear cache and retry
walkeros bundle flow.json --no-cache
# Check npm registry access
npm pingBuild Issues
If you encounter build issues:
# Clear cache and retry
walkeros cache clear
# Retry with verbose output
walkeros bundle flow.json --verbosePort Already in Use
If the port is already in use:
# Start the built flow on a different port
runneros start dist/flow.mjs --port 8081
# Or kill the process using the port
lsof -ti:8080 | xargs killAuthentication Issues
If app commands fail with authentication errors:
# Check current identity
walkeros auth whoami
# Re-authenticate
walkeros auth login
# Or use an automation token from Account, Automation tokens
export WALKEROS_TOKEN=wos_pat_xxxnft cannot trace a runtime asset
If a runtime asset is missing in dist/node_modules/ (typically because the dep loads it via a dynamically constructed path), declare it explicitly under the flow's config.bundle.traceInclude. Paths and globs both work, resolved against the install root:
"flows": {
"default": {
"config": {
"platform": "server",
"bundle": {
"packages": { "@walkeros/server-destination-gcp": {} },
"traceInclude": [
"node_modules/some-pkg/data/*.json"
]
}
}
}
}Caching pacote downloads in CI
The bundler caches downloads under process.env.NPM_CACHE_DIR (default
$TMPDIR/cache/npm). On CI, persist that path with actions/cache:
- uses: actions/cache@v4
with:
path: .walkeros-cache/npm
key: walkeros-${{ hashFiles('**/flow.json') }}
- run: npx walkeros bundle flow.json -o dist/
env:
NPM_CACHE_DIR: ${{ github.workspace }}/.walkeros-cache/npmBundle output is large
dist/node_modules/ for a server flow with the GCP destination typically holds 30-50MB across 10k+ files (grpc, protobufjs, etc.). Recommendations:
- Use
.dockerignoreto exclude irrelevant build output from non-bundle contexts. - Prefer
COPY --from=builder /build/dist/ /app/flow/over file-by-file in your Dockerfile. - Expect ~30s artifact upload time on slow CI.
Next steps
- Flow Configuration - Learn about flow config structure
- Runtime - Run built flows with
runnerosand thewalkeros/flowimage - MCP Server - Use CLI tools from AI assistants
- Sources - Explore event sources
- Destinations - Configure analytics tools
- Mapping - Transform events for destinations
See Also
Using Integrated mode instead? The CLI uses JSON configuration (Bundled mode). If you prefer TypeScript and want the collector built into your application, see Integrated Mode and the Collector documentation.
Both approaches use the same underlying architecture. The difference is how you configure and deploy.