> Part of the walkerOS documentation. Project overview and full index: <https://www.walkeros.io/llms.txt>

# 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[​](#what-changed-at-a-glance "Direct link to 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`).
* **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[​](#step-by-step-json-migration "Direct link to Step-by-step JSON migration")

The transforms are independent; apply them in any order.

### Transform 1: bump `version`[​](#transform-1-bump-version "Direct link to 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`[​](#transform-2-update-schema "Direct link to 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`[​](#transform-3-lift-web--server-into-config "Direct link to 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). The `windowElb` key is deprecated in v4; when set, its value is forwarded to the browser source with a warning. 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`[​](#transform-4-lift-bundle-into-config "Direct link to 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[​](#worked-example "Direct link to 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 old shapes. Only the platform/bundle plumbing moves.

## The new `$flow.X.Y` reference[​](#the-new-flowxy-reference "Direct link to the-new-flowxy-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 bundle` and `walkeros deploy` error if `$flow.X.Y` resolves to an empty value.
* `walkeros validate` warns by default, escalates to error with `--strict`.
* The error message points you to the source: set `flows.server.config.url`, or run `walkeros deploy server` first.

## TypeScript type renames[​](#typescript-type-renames "Direct link to 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[​](#no-automated-codemod "Direct link to 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[​](#walker-commands-take-an-init-object "Direct link to 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`[​](#walker-destination "Direct link to 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`[​](#walker-hook "Direct link to 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`[​](#walker-on "Direct link to 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[​](#programmatic-apis-drop-the-options-argument "Direct link to 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, destination)` (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>"`[​](#step-shape-import-replaces-code-name "Direct link to 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`: `import` set without `package`.
* `OBSOLETE_CODE_STRING`: legacy `code: "<name>"` form. Rename hint points to `import`.
* `INVALID_IMPORT`: `import` is not a non-empty string.
* `INVALID_CODE_SHAPE`: `code` is not an object with a `push` function.
* `UNKNOWN_KEY` and `CONFLICT` are retained from earlier validators.
