Skip to main content
Ask your AI

Migrating flow.json from v3 to v4

walkerOS Flow v4 reshapes flow.json so each flow has a dedicated config block (platform, url, settings, bundle), and adds the $flow.X.Y reference syntax for cross-flow lookups inside the same file. The v4 CLI rejects a file with "version": 3, so every existing flow.json needs a one-time edit.

This guide walks through the JSON transforms, then covers the TypeScript type renames for code that imports Flow.* from @walkeros/core, the changed event fields and the changed runtime commands.

What changed at a glance​

  • Type renames in @walkeros/core: Flow.Settings (whole flow) becomes Flow, Flow.Config (root file) becomes Flow.Json, and the new Flow.Settings is now the small key-value bag inside Flow.Config.settings.
  • New per-flow config block with platform, url, settings, and bundle.
  • web and server keys are gone. Their fields move into config.platform and config.settings.
  • bundle is no longer a top-level flow key. It moves into config.bundle.
  • New $flow.X.Y reference syntax lets one flow read another flow's resolved config (e.g., $flow.server.url).
  • Inside steps: the mapping rule field skip is now silent, the references $store:id and $secret:NAME are now $store.id and $secret.NAME, and mapping callbacks take (value, context).
  • Event fields: event.id is a span id, group, count and version are gone, and source has new fields (see Event fields).
  • Hard cut: the v4 CLI refuses "version": 3. There is no compat shim.
  • No automated migration command. Edits are mechanical, see below.

Step-by-step JSON migration​

The transforms are independent; apply them in any order.

Transform 1: bump version​

{
"version": 4
}

Was "version": 3. walkeros validate and walkeros bundle fail on any other value with version: Invalid input: expected 4.

Transform 2: update $schema​

{
"$schema": "https://walkeros.io/schema/flow/v4.json"
}

Was https://walkeros.io/schema/flow/v3.json.

Transform 3: lift web / server into config​

Every flow had either a "web": { ... } or a "server": { ... } block. Both are gone in v4. Replace them with a config block that carries platform plus any web settings:

v3:

{
"flows": {
"default": {
"web": {
"windowCollector": "collector",
"windowElb": "elb"
}
}
}
}

v4:

{
"flows": {
"default": {
"config": {
"platform": "web",
"settings": {
"windowCollector": "collector"
}
}
}
}
}

The v3 windowElb name moves to the browser source: set elb in the browser source's config.settings (default "elb", so it can usually be dropped). Move it rather than carrying it into config.settings. A windowElb left there is handled by walkeros bundle with a warning: its value is forwarded to the browser source's elb when that source sets none, and ignored when the source already sets elb or the flow has no browser source. Keeping windowCollector preserves your existing global; dropping it uses the v4 default, window.walkerOS.

For server flows, the transform is the same: drop "server": {}, add "config": { "platform": "server" }. Server flows can also set config.url (used by $flow.X.url lookups, see below).

Transform 4: lift bundle into config​

In v3, bundle was a top-level sibling of web / server. In v4, it moves under config:

v3:

{
"flows": {
"default": {
"web": {},
"bundle": {
"packages": {
"@walkeros/collector": { "version": "latest" }
}
}
}
}
}

v4:

{
"flows": {
"default": {
"config": {
"platform": "web",
"bundle": {
"packages": {
"@walkeros/collector": { "version": "latest" }
}
}
}
}
}
}

bundle.overrides (transitive dependency pins) lives in the same place, under config.bundle.overrides.

Transform 5: rename the mapping rule field skip to silent​

A mapping rule that ran its side effects without the destination's default call used skip: true. In v4 the field is silent:

{
"page": {
"view": { "silent": true }
}
}

walkeros validate does not report a leftover skip. The rule then has no effect, and the destination sends its default call for that event.

Transform 6: use a dot in $store and $secret references​

$store:id is now $store.id, and $secret:NAME is now $secret.NAME. The other references ($var., $env., $contract., $code:) are unchanged. walkeros validate warns on the colon form, for example Malformed reference "$secret:API_TOKEN", use a dot, not a colon: "$secret.API_TOKEN"., and --strict turns the warning into a failure.

Transform 7: callback arguments in $code:​

Mapping fn, condition and validate callbacks take (value, context). The v3 second argument mapping is now context.mapping, and the context also carries event, collector, logger and consent. A rule-level condition takes (event, context):

{
"fn": "$code:(value, context) => context.mapping"
}

A v3 callback such as (value, mapping, options) => ... still runs, but its second argument is now the context object and its third is undefined.

Worked example​

A small v3 web flow:

{
"version": 3,
"$schema": "https://walkeros.io/schema/flow/v3.json",
"flows": {
"default": {
"web": {
"windowCollector": "collector",
"windowElb": "elb"
},
"bundle": {
"packages": {
"@walkeros/collector": { "version": "latest", "imports": ["startFlow"] },
"@walkeros/web-destination-gtag": { "version": "latest", "imports": ["destinationGtag"] }
}
},
"destinations": {
"ga4": {
"package": "@walkeros/web-destination-gtag",
"config": {
"settings": { "ga4": { "measurementId": "G-XXXXXXXXXX" } }
}
}
}
}
}
}

The same flow in v4:

{
"version": 4,
"$schema": "https://walkeros.io/schema/flow/v4.json",
"flows": {
"default": {
"config": {
"platform": "web",
"settings": {
"windowCollector": "collector"
},
"bundle": {
"packages": {
"@walkeros/collector": { "version": "latest", "imports": ["startFlow"] },
"@walkeros/web-destination-gtag": { "version": "latest", "imports": ["destinationGtag"] }
}
}
},
"destinations": {
"ga4": {
"package": "@walkeros/web-destination-gtag",
"config": {
"settings": { "ga4": { "measurementId": "G-XXXXXXXXXX" } }
}
}
}
}
}
}

Notice that sources, destinations, transformers, stores, and collector keep their shapes, apart from transforms 5 to 7 and the step import field (see Step shape). Only the platform/bundle plumbing moves.

The new $flow.X.Y reference​

$flow.<flowName>(.<path>)? resolves to the value at flows.<flowName>.config.<path> in the same flow.json file. The config segment is implicit, so $flow.server.url walks to flows.server.config.url. This is useful when a web flow needs to point its API destination at the URL of a sibling server flow:

{
"version": 4,
"flows": {
"server": {
"config": {
"platform": "server",
"url": "https://api.example.com/collect"
}
},
"web": {
"config": { "platform": "web" },
"destinations": {
"api": {
"package": "@walkeros/web-destination-api",
"config": { "settings": { "url": "$flow.server.url" } }
}
}
}
}
}

walkeros validate and walkeros bundle both fail when a $flow reference does not resolve. Without flows.server.config.url, the message is Path "url" not found in "$flow.server": "url" does not exist; an unknown flow name fails with Flow "<name>" not found in $flow.<name>. Set config.url yourself: deploying a flow does not fill it in.

TypeScript type renames​

If your code imports Flow.* types from @walkeros/core, apply this rename catalog. Sub-namespaces are gone; everything is flat under Flow.

v3v4
Flow.Config (root file)Flow.Json
Flow.Settings (single flow)Flow (interface)
(none)Flow.Settings (NEW: kv-bag inside Flow.Config)
Flow.Web, Flow.Server(removed) config.platform is a string
Flow.InlineCodeFlow.Code
Flow.PackagesFlow.Bundle.packages
Flow.OverridesFlow.Bundle.overrides
Flow.SourceReferenceFlow.Source
Flow.DestinationReferenceFlow.Destination
Flow.TransformerReferenceFlow.Transformer
Flow.StoreReferenceFlow.Store
Flow.ContractEntryFlow.ContractRule

Step-related types (Flow.StepExample, Flow.StepExamples, Flow.StepCommand, Flow.StepEffect, Flow.StepOut) are unchanged. Flow.ContractSchema, Flow.ContractActions, and Flow.ContractEvents are also unchanged.

A typical TypeScript change looks like this:

// v3
import type { Flow } from '@walkeros/core';
function buildFlow(): Flow.Settings {
return { web: {}, destinations: {} };
}
// v4
import type { Flow } from '@walkeros/core';
function buildFlow(): Flow {
return { config: { platform: 'web' }, destinations: {} };
}

No automated codemod​

walkerOS v4 does not ship a walkeros migrate command. The transforms are mechanical string-level edits, and even complex configs produce a small, readable diff. Apply them manually, then run walkeros validate.

Validate does not catch every v3 leftover. It reports a "version": 3 and a colon reference, but it ignores a web, server or bundle key left at the top level of a flow, and a mapping rule's skip. Search for them by hand. walkeros bundle stops on a flow without config.platform (Flow must have config.platform set to "web" or "server"), and it ignores a top-level bundle without a message, so the package versions and overrides in it are not applied.

Event fields​

Events have a new shape in v4. Update every mapping key, contract and downstream query that reads one of these fields:

  • event.id is a W3C Trace Context span id (16 lowercase hex characters), generated by the collector per event.
  • event.group, event.count and event.version are removed. Use source.trace in place of group: a W3C trace id (32 lowercase hex characters) minted on each run, shared by every event of that run and kept across the web to server hop. Use source.count in place of count, the per-run sequence.
  • source.type is the source kind (browser, dataLayer, express, ...), and the new source.platform is the runtime (web or server).
  • source.id and source.previous_id are removed. The browser source sets source.url and source.referrer in their place.

Walker commands take an Init object​

Three runtime elb('walker ...') commands changed shape in v4. The positional and shorthand forms are removed, every one of them now takes a single Init object. The walker hook command also has a real runtime handler now (in v3 it accepted arguments but never wired anything up).

A call in a v3 form changes nothing and still answers ok: true. With a third argument, the collector logs elb ignored an argument it does not take: ... at ERROR. Without one, for example elb('walker destination', destination) or the { push } shorthand, it adds no destination and logs nothing.

walker destination​

Was: elb('walker destination', destination, config?), or the { push } shorthand, or { code } plus a separate config argument.

Now: a single Init object with code (the destination) and an optional config.

// v3
elb('walker destination', destinationGtag, {
settings: { ga4: { measurementId: 'G-XXX' } },
});

// v4
elb('walker destination', {
code: destinationGtag,
config: {
settings: {
ga4: { measurementId: 'G-XXX' },
},
},
});

walker hook​

Was: elb('walker hook', name, fn), which v3 accepted without registering the hook.

Now: elb('walker hook', { name, fn }), and the hook runs.

// v3
elb('walker hook', 'prePush', (params, ...args) => params.fn(...args));

// v4
elb('walker hook', {
name: 'prePush',
fn: (params, ...args) => params.fn(...args),
});

walker on​

Was: elb('walker on', type, rules).

Now: elb('walker on', { type, rules }). The callbacks take (data, context): the v3 first argument collector is now context.collector, next to context.logger.

// v3
elb('walker on', 'consent', {
marketing: (collector, consent) => {
// ...
},
});

// v4
elb('walker on', {
type: 'consent',
rules: {
marketing: (consent, context) => {
// ...
},
},
});

Programmatic APIs drop the options argument​

If your code calls into the collector directly (rather than through elb), three signatures lost their third options parameter:

  • collector.command(name, data) (was command(name, data, options))
  • addDestination(collector, { code, config }) (was addDestination(collector, destination, config))
  • commonHandleCommand(collector, name, data) (was commonHandleCommand(collector, name, data, options))

For walker destination, pass the config through the Init object's config field instead of a third argument. For walker run, the runState partial is now optional (elb('walker run') with no second argument is valid).

Step shape: import replaces code: "<name>"​

Every step (source, transformer, destination, store) now accepts a new import?: string field. With package, import selects a named export from that package. Using package alone still loads the default export. Inline code stays object-only: code: { push, type?, init? }.

The v3 string form code: "<exportName>" is rejected. Rename it to import: "<exportName>":

"destinations": {
"bigquery": {
"package": "@walkeros/server-destination-gcp",
- "code": "destinationBigQuery",
+ "import": "destinationBigQuery",
"config": { "settings": { ... } }
}
}

walkeros validate reports these step shapes:

  • code: "<name>": code: Invalid input: expected object, received string.
  • import without package: import: `import` requires `package` .
  • An import that is not a JavaScript identifier: import: import must be a valid JavaScript identifier.
  • code together with package: Cannot specify both package and code.
  • An unknown key on a destination: Unknown key "<key>" on Destination is ignored by the runtime.
💡 Need implementation support?
elbwalker offers hands-on support: setup review, measurement planning, destination mapping, and live troubleshooting. Book a 2-hour session (€399)