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 v3
input outright, so every existing flow.json needs a one-time edit.
This guide walks through the four mechanical JSON transforms, then covers
the matching TypeScript type renames for code that imports Flow.* from
@walkeros/core.
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). - Hard cut: the v4 CLI refuses v3 input. 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. The v4 CLI fails fast on any other value with a clear
unsupported flow.json version error.
Transform 2: update $schema
{
"$schema": "https://walkeros.io/schema/flow/v4.json"
}
Was https://walkeros.io/schema/flow/v3.json. The v3 schema file has been
removed from the docs site.
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",
"windowElb": "elb"
}
}
}
}
}
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.
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",
"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" } }
}
}
}
}
}
}
Notice that sources, destinations, transformers, stores, and
collector keep their old shapes. 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" } }
}
}
}
}
}
Strictness rules:
walkeros bundleandwalkeros deployerror if$flow.X.Yresolves to an empty value.walkeros validatewarns by default, escalates to error with--strict.- The error message points you to the source: set
flows.server.config.url, or runwalkeros deploy serverfirst.
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 (four edits per file, all string-level), and even complex
configs produce a small, readable diff. Apply the four transforms
manually, run walkeros validate to confirm the result parses, and
you're done.
If validate reports an error, the message will name the exact path that
still looks like v3, e.g., a stray top-level bundle or a web block
left behind.
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).
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) (no-op at runtime, the dispatch
silently dropped the call).
Now: elb('walker hook', { name, fn }), and the hook actually runs.
// v3 (silently ignored)
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 }).
// v3
elb('walker on', 'consent', {
marketing: (consent, context) => {
// ...
},
});
// 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, destination)(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 legacy string form code: "<exportName>" is no longer accepted.
Rename it to import: "<exportName>":
"destinations": {
"bigquery": {
"package": "@walkeros/server-destination-gcp",
- "code": "destinationBigQuery",
+ "import": "destinationBigQuery",
"config": { "settings": { ... } }
}
}
Empty step entries (no package, import, code, or chain fields) are
now valid no-ops in any of the four step kinds.
Validation raises one of these error codes when a step shape is invalid:
MISSING_PACKAGE:importset withoutpackage.OBSOLETE_CODE_STRING: legacycode: "<name>"form. Rename hint points toimport.INVALID_IMPORT:importis not a non-empty string.INVALID_CODE_SHAPE:codeis not an object with apushfunction.UNKNOWN_KEYandCONFLICTare retained from earlier validators.