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) becomesFlow,Flow.Config(root file) becomesFlow.Json, and the newFlow.Settingsis now the small key-value bag insideFlow.Config.settings. - New per-flow
configblock withplatform,url,settings, andbundle. webandserverkeys are gone. Their fields move intoconfig.platformandconfig.settings.bundleis no longer a top-level flow key. It moves intoconfig.bundle.- New
$flow.X.Yreference syntax lets one flow read another flow's resolvedconfig(e.g.,$flow.server.url). - Inside steps: the mapping rule field
skipis nowsilent, the references$store:idand$secret:NAMEare now$store.idand$secret.NAME, and mapping callbacks take(value, context). - Event fields:
event.idis a span id,group,countandversionare gone, andsourcehas 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.
| v3 | v4 |
|---|---|
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.InlineCode | Flow.Code |
Flow.Packages | Flow.Bundle.packages |
Flow.Overrides | Flow.Bundle.overrides |
Flow.SourceReference | Flow.Source |
Flow.DestinationReference | Flow.Destination |
Flow.TransformerReference | Flow.Transformer |
Flow.StoreReference | Flow.Store |
Flow.ContractEntry | Flow.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.idis a W3C Trace Context span id (16 lowercase hex characters), generated by the collector per event.event.group,event.countandevent.versionare removed. Usesource.tracein place ofgroup: 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. Usesource.countin place ofcount, the per-run sequence.source.typeis the source kind (browser,dataLayer,express, ...), and the newsource.platformis the runtime (weborserver).source.idandsource.previous_idare removed. The browser source setssource.urlandsource.referrerin 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)(wascommand(name, data, options))addDestination(collector, { code, config })(wasaddDestination(collector, destination, config))commonHandleCommand(collector, name, data)(wascommonHandleCommand(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.importwithoutpackage:import: `import` requires `package`.- An
importthat is not a JavaScript identifier:import: import must be a valid JavaScript identifier. codetogether withpackage:Cannot specify both package and code.- An unknown key on a destination:
Unknown key "<key>" on Destination is ignored by the runtime.