Skip to Content
DocsReferenceFlow JSON Schema

Flow JSON Schema

The canonical schema for .flow.yaml and .flow.json files. Editors consume it for autocomplete, hover docs, and inline validation; CI consumes it as the validation gate. This page is the field-by-field reference. For prose, narrative, and worked examples per step, see IR reference.

What it is

The Flow IR is a Zod schema in @klera/protocol. A JSON Schema 7 projection is derived mechanically from that Zod tree — the Zod schema stays canonical, JSON Schema is regenerated on every CLI invocation, so drift is impossible.

The same JSON Schema covers both authoring forms:

  • .flow.yaml — hand-authored, validated by the YAML language server via the # yaml-language-server: $schema=... directive.
  • .flow.json — the prose-compiler cache (an IR plus a _meta envelope). Plumbing; you don’t hand-edit it.

How to get it

klera schema --out .klera/flow.schema.json --pretty

klera init runs this for you and scaffolds the directive into the first flow file, so adopters never have to think about it. To wire a specific editor, see editor support.

klera schema always emits the schema for the installed @klera/protocol version. Editor validation tracks your dependency version — bump the package, regenerate, and your editor sees the new step keywords automatically.

Top-level shape

A flow is one object with a name, an optional description, and a non-empty steps array. The other top-level fields tune execution defaults.

type Flow = { name: string; // required, min length 1 description?: string; defaultTimeoutMs: number; // default 10_000, max 600_000 selfHealing: boolean; // default true parallel?: Array<'ios' | 'android' | 'web'>; // lockstep cross-platform divergenceTolerance?: DivergenceTolerance; // tuned for `parallel` steps: Step[]; // min length 1 };

The cache form (.flow.json) wraps a Flow in a _meta envelope:

type SemanticPlan = { // ...all Flow fields above _meta: { promptHash: string; // sha256 of the prose body snapshotHash: string; // sha256 of the element-graph snapshot combined: string; // sha256(promptHash|snapshotHash|plannerVersion) plannerVersion: string; // e.g. "0.1.0" model?: string; // "anthropic" | "manual" | "via-cli" | ... }; };

combined is what klera compile --check reads to decide whether the cache is stale; the individual hashes narrow the staleness reason for the diff renderer.

Step variants

Every variant is keyed by its discriminating field. Each is .strict(), so unknown keys fail validation loudly. Targets accept the short form (a bare string → text match) or the long form ({ testID?, text?, role?, accessibilityLabel?, scope? }).

KeywordRequiredOptional
taptarget
longPresstarget
typeinto, value
scrollto, direction (default down), maxSwipes (10)
assertone of visible / notVisible / hasText
waitseconds or fortimeoutSeconds (10)
waitForIdleanimationsnetworktimeoutMs, quietWindowMs
dismissAlertlabel string
dismissActionSheetlabel string
dismissKeyboardtrue
swipedirection enum or from+todurationMs (300)
tapCoordx, ydurationMs (60)
multiTappoints (2–5)durationMs (60)
pinchfrom (2-tuple), to (2-tuple)durationMs (300)
screenshotpathscale
visualSnapshotidtolerance (0.5), region
setOrientationportraitlandscape
openURLURL string
setClipboardstring
setLocationlat, lon
setBiometricoutcome (success/failure/not-enrolled)
relaunchtrue or {}args[], url
backgroundApptrue
foregroundApptrue
pressBacktrue (Android-only)
grantPermissionone of camera/microphone/location/notifications
mockNetworknon-empty NetworkMockSpec[]per-spec: status (200), delayMs, headers, body/fixture
unmockNetworktrue or { method, path }[]
assertNetworkCalledat least one of method / pathcount (1), bodyContains, headersContain
assertJSexpressiontimeoutMs (2000); at most one of equals/notEquals/matches/contains/gt/lt/gte/lte
optionalwhen.visible, do (a basic step)

For the prose narrative on each step — when to reach for it, the gotchas, sample flows — see IR reference. This page is the field shape only.

Validation rules

Step variants are .strict(). Steps with two top-level keywords fail with “Unrecognized key” instead of silently parsing as the first variant and dropping the rest. The planner’s retry loop surfaces this Zod error back to the LLM as actionable feedback.

A few rules worth pinning down explicitly:

  • A target short form is a non-empty string. The long form requires at least one of testID, text, or accessibilityLabel.
  • assert requires at least one of visible / notVisible / hasText.
  • waitForIdle: false is rejected — opt out by omitting the step.
  • waitForIdle long form must enable at least one of animations or network.
  • assertJS long form accepts at most one comparator.
  • assertNetworkCalled requires at least one of method or path.
  • pressBack and grantPermission are Android-only; iOS drivers reject with capability_unsupported at runtime — the schema doesn’t gate platform.
  • optional.do accepts any basic step except another optional (no nesting).

Versioning

The schema versions in lockstep with @klera/protocol. The build hosting this docs site renders against 0.1.0 today. To check the version shipping in your project:

node -p "require('@klera/protocol/package.json').version"

klera schema always emits the schema for the installed protocol version, so editor validation tracks your dependency. When you bump @klera/protocol, rerun klera schema --out .klera/flow.schema.json (or let klera init --force redo it) and your editor picks up the new shape on the next reload.

Programmatic access

import { flowSchema } from '@klera/protocol'; const schema = flowSchema(); // JSON Schema 7 object

The function form keeps the conversion lazy — the protocol package’s eager import graph stays small for adopters who never call it. Useful for plugin authors, custom validators, and anyone generating their own editor tooling.

The companion constant for the YAML directive:

import { YAML_LANGUAGE_SERVER_DIRECTIVE } from '@klera/protocol'; // → "# yaml-language-server: $schema="

Worked example

A small .flow.yaml with the schema directive at the top:

# yaml-language-server: $schema=../.klera/flow.schema.json name: Log in and see welcome description: Sanity check the seeded test user lands on home. defaultTimeoutMs: 15000 steps: - tap: Sign In - type: { into: { testID: email-field }, value: user@test.com } - tap: Continue - waitForIdle: { animations: true, network: true, timeoutMs: 5000 } - assert: { visible: Welcome back } - visualSnapshot: home-after-login

The fragment of the JSON Schema that validates the type step on line 7:

{ "TypeStep": { "type": "object", "additionalProperties": false, "required": ["type"], "properties": { "type": { "type": "object", "required": ["into", "value"], "properties": { "into": { "anyOf": [ { "type": "string", "minLength": 1 }, { "$ref": "#/definitions/TargetSpec" } ] }, "value": { "type": "string" } } } } } }

The editor reads this, sees into accepts the short or long form, and offers testID / text / role / accessibilityLabel / scope as completions inside the long-form object. additionalProperties: false is the JSON Schema projection of Zod’s .strict() — paste a typo like tetID and the editor underlines it before the test ever runs.

Last updated on