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_metaenvelope). Plumbing; you don’t hand-edit it.
How to get it
klera schema --out .klera/flow.schema.json --prettyklera 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.
TypeScript (Zod-derived)
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? }).
| Keyword | Required | Optional |
|---|---|---|
tap | target | — |
longPress | target | — |
type | into, value | — |
scroll | — | to, direction (default down), maxSwipes (10) |
assert | one of visible / notVisible / hasText | — |
wait | seconds or for | timeoutSeconds (10) |
waitForIdle | animations ‖ network | timeoutMs, quietWindowMs |
dismissAlert | label string | — |
dismissActionSheet | label string | — |
dismissKeyboard | true | — |
swipe | direction enum or from+to | durationMs (300) |
tapCoord | x, y | durationMs (60) |
multiTap | points (2–5) | durationMs (60) |
pinch | from (2-tuple), to (2-tuple) | durationMs (300) |
screenshot | path | scale |
visualSnapshot | id | tolerance (0.5), region |
setOrientation | portrait ‖ landscape | — |
openURL | URL string | — |
setClipboard | string | — |
setLocation | lat, lon | — |
setBiometric | outcome (success/failure/not-enrolled) | — |
relaunch | true or {} | args[], url |
backgroundApp | true | — |
foregroundApp | true | — |
pressBack | true (Android-only) | — |
grantPermission | one of camera/microphone/location/notifications | — |
mockNetwork | non-empty NetworkMockSpec[] | per-spec: status (200), delayMs, headers, body/fixture |
unmockNetwork | true or { method, path }[] | — |
assertNetworkCalled | at least one of method / path | count (1), bodyContains, headersContain |
assertJS | expression | timeoutMs (2000); at most one of equals/notEquals/matches/contains/gt/lt/gte/lte |
optional | when.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
targetshort form is a non-empty string. The long form requires at least one oftestID,text, oraccessibilityLabel. assertrequires at least one ofvisible/notVisible/hasText.waitForIdle: falseis rejected — opt out by omitting the step.waitForIdlelong form must enable at least one ofanimationsornetwork.assertJSlong form accepts at most one comparator.assertNetworkCalledrequires at least one ofmethodorpath.pressBackandgrantPermissionare Android-only; iOS drivers reject withcapability_unsupportedat runtime — the schema doesn’t gate platform.optional.doaccepts any basic step except anotheroptional(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 objectThe 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-loginThe 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.