OpenWOP openwop.dev
FieldValue
RFC0136
TitleWorkflowVariable.format — a presentational hint for run inputs
StatusAccepted
Author(s)openwop-app maintainers (corpus half + question rulings: David Tufts / @davidscotttufts)
Created2026-08-01
Updated2026-08-11 — Active → Accepted; reference-host propagation (openwop-app#3131) + non-vacuous witness under OPENWOP_REQUIRE_BEHAVIOR=true (see §"Acceptance witness"). 2026-08-10 — Draft → Active; corpus half (steps 1–3) landed, three open questions resolved into normative text. 2026-08-01 — filed Draft.
Affectsschemas/workflow-definition.schema.json (§WorkflowVariable), schemas/workflow-chain-pack-manifest.schema.json (chain parameters), conformance scenarios
Compatibilityadditive per COMPATIBILITY.md
Supersedes
Superseded by

Summary

WorkflowVariable carries type (string | number | boolean | object | array) but nothing finer. A host rendering a workflow's declared run inputs therefore cannot tell an email address from a URL from a free-text note — all three are type: "string" — and must render every one as a plain text field. Because WorkflowVariable is additionalProperties: false, a host cannot carry the distinction out-of-band either; the information has nowhere to live on the wire.

This RFC adds an optional format string to WorkflowVariable, drawn from the JSON-Schema format vocabulary. It is advisory and presentational: it MUST NOT be used to reject a run.

Motivation

This is a real, currently-shipping gap, not a hypothetical. In the openwop-app reference host, 10+ workflow chains declare a recipientEmail parameter and every chain that sends mail declares senderEmail. Both are unambiguously email addresses. Every host renders them as <input type="text">, so:

  • mobile keyboards show no @ key — the single highest-friction moment in

filling out a run form on a phone;

  • the browser's free email validation never runs, so a typo becomes a failed

send discovered only after the run;

  • assistive technology gets no semantic hint beyond "text field";
  • hosts cannot compensate. A host that guesses from the variable name

(/email$/i) misfires on a name like emailTemplateId, and the guess is not portable — every other host has to invent the same heuristic independently.

The last point is what makes this a wire concern rather than a UI one. The authoring side (a chain pack, an SDK, a workflow author) knows the field is an email address. There is simply no slot to say so, so each host re-derives it badly or not at all.

Precedent

sensitive was added to this exact object by RFC 0124 for the same structural reason: hosts needed a per-variable property the wire did not carry, and additionalProperties: false meant it could only arrive via an RFC. format is the same shape of change — one optional, additive, per-variable property — with a strictly smaller blast radius, because unlike sensitive it changes no persistence, masking, or replay behaviour.

Design

Add to $defs.WorkflowVariable in schemas/workflow-definition.schema.json:

"format": {
  "type": "string",
  "description": "Advisory presentational hint for a `type: \"string\"` variable, drawn from the JSON-Schema format vocabulary. Hosts SHOULD use it to choose an input affordance (e.g. an email keyboard). It is NOT a validation contract: hosts MUST NOT reject a run because a value does not match, and MUST NOT assume a value matches when reading it."
}

Recognised values (the closed set for v1 — extending it is a further RFC):

formatMeaningTypical affordance
emaila single email addresstype="email"
urian absolute URItype="url"
dateRFC 3339 full-datetype="date"
date-timeRFC 3339 date-timetype="datetime-local"
timeRFC 3339 full-timetype="time"
durationISO 8601 durationtext + hint

Normative requirements

1. format MUST be ignored when type is not "string". 2. A host that does not recognise a format value MUST fall back to plain text rendering. An unknown format MUST NOT be an error. 3. A host MUST NOT reject a run, refuse a variable write, or fail validation on the grounds that a value does not match its declared format. format is a hint about intent, never a guarantee about data. 4. A host MUST NOT infer format from a variable's name. Name-based inference is what this field exists to replace, and it produces wrong answers (emailTemplateId is not an email address). 5. format MUST survive :fork and replay verbatim, like every other WorkflowVariable property. It is authoring-time data and is never re-derived. 6. format is orthogonal to sensitive. A variable MAY carry both. sensitive masks the value on server-emitted surfaces (variable.changed, state.snapshot, RunSnapshot.variablesobservability.md §Privacy classification); format describes the field in the definition, which is not a masked surface. A host MUST NOT suppress, alter, or refuse a format because the variable is sensitive, and MUST NOT infer sensitivity from a format. 7. Chain-parameter propagation is scoped to deferred mode. A host expanding a workflow-chain pack in deferred-parameter mode (RFC 0124, capabilities.workflowChainPacks.deferredParameters.supported: true) MUST copy a string parameter's JSON-Schema format verbatim onto the WorkflowVariable it materializes, alongside type / defaultValue / required (workflow-chain-packs.md §"Deferred-parameter expansion" step 1). The copy is verbatim and unvalidated: an unrecognised format is still copied, because the destination host may recognise it. Under expansion-time substitution (the floor) the requirement does not apply and cannot — that mode freezes parameters into config and mints no WorkflowVariable to carry a hint. 8. format MUST NOT participate in a configurable validation decision. A host MAY carry format into a workflow's configurableSchema as an annotation — it is a JSON Schema 2020-12 document where format is legal, and run-options.md §2 surfaces it on GET /v1/workflows/{workflowId} for clients to pre-flight against, which is a legitimate place for the hint to reach a run-input form. But run-options.md §1 makes validating RunOptions.configurable against that schema a MUST, with rejection via validation_error on failure — so a host whose validator treats format as an assertion (the default in several common libraries, though JSON Schema 2020-12 itself specifies annotation-only) would reject a run on a format mismatch, violating requirement 3 through the back door. Requirement 3 is absolute and surface-independent: a format mismatch MUST NOT fail a run no matter which schema the format was read from. A host that cannot guarantee annotation-only treatment MUST NOT propagate format into configurableSchema.

Why advisory rather than validating

A validating format would be a breaking change in effect if not in shape: existing runs carry values that were never checked against it, and a host that began enforcing email would start refusing workflows that ran yesterday. It would also duplicate validation that belongs to the node actually consuming the value, which is the only component that knows what it can accept. Keeping format strictly presentational means a host can adopt it incrementally with zero risk to existing runs.

Alternatives considered

1. Do nothing; let each host guess from the name. Rejected — it is what happens today. It misfires (emailTemplateId), it is unportable (every host reinvents it), and it silently degrades for authors who name fields differently.

2. Reuse type with finer values (type: "email"). Rejected — type is a JSON-Schema type, and overloading it breaks every consumer that switches on the existing five values, including SDK type generation. Breaking, for no gain over an additive sibling property.

3. Put it in description. Rejected — description is human-facing prose rendered as field help. Encoding machine-readable intent in it means parsing English, and it is already localised in some hosts.

4. A separate presentation object. Rejected as premature: one field is needed now, and a container invites unbounded growth on a wire object without a driving use case for the rest of it.

Compatibility

additive per COMPATIBILITY.md §2.2:

  • optional property, absent by default — every existing WorkflowVariable remains

valid unchanged;

  • no existing property changes meaning or type;
  • no error code changes meaning;
  • a host that ignores format entirely stays conformant — the only consequence is

that it keeps rendering plain text fields, which is exactly today's behaviour.

No migration is required for stored definitions.

Conformance

A scenario asserting:

  • a definition carrying format: "email" round-trips through

POST /v1/workflowsGET /v1/workflows/{id} verbatim;

  • an unrecognised format value round-trips verbatim and does not error

(requirement 2);

  • a run whose variable value does not match its declared format is accepted

and completes (requirement 3) — the assertion that keeps format advisory;

  • format survives :fork (requirement 5).

Resolved questions

All three were settled against the corpus before the Active flip rather than carried into it; each ruling is now normative text, not a register row.

1. Closed recognised-value table, or open to any JSON-Schema format token? Ruled: the wire is OPEN; the table is a recognition registry, not a validation constraint. The question conflated two axes. A workflow definition is a client-submitted shape, which COMPATIBILITY.md §"Schema closure" (RFC 0094) keeps closed (additionalProperties: false on WorkflowVariable — the very fact that motivates this RFC). Declaring format as an enum on a closed shape would make an unrecognised value a hard POST /v1/workflows validation failure — which directly contradicts requirement 2 ("An unknown format MUST NOT be an error"). The schema therefore declares format as a plain string, and the six-row table is the set hosts are expected to recognise, not the set the wire accepts. Requirement 2 does carry the weight, as the question anticipated — and conformance leg A2 is exactly the test that keeps it carrying it.

2. Is chain-parameter format propagation a MUST for all hosts? Ruled: a MUST, but scoped to deferred-parameter mode — it cannot be universal. The premise that hosts mint a WorkflowVariable from a chain parameter is only true in RFC 0124's capability-gated deferred mode. Expansion-time substitution — "the default and the floor" per workflow-chain-packs.md — freezes parameter values into node config and creates no variable at all, so on a floor host there is no object for format to propagate into. Landed as requirement 7 and in the §"Deferred-parameter expansion" step-1 copy list, which already enumerates type / defaultValue / required; format joins that list. Confirmed against the schema: WorkflowChain.parameters is type: object, additionalProperties: true — a free-form JSON Schema fragment — so format was already legal there and no schema change was required for step 2. The RFC's own framing of step 2 was accurate.

Amended 2026-08-10 after a host-side finding. The reference host reported that format is dropped at two sites, not one: variables[] materialization and configurableSchema, and proposed carrying it to both so the field would be "enforced". The observation is right and the conclusion inverts the design. format is never enforced — requirement 3 forbids it — and configurableSchema is precisely the surface where propagation could accidentally enforce it, because run-options.md §1 makes validating configurable against that schema a MUST with validation_error on failure. Carrying format there and letting a format-asserting validator see it converts an advisory hint into a run-rejection path. Landed as requirement 8: annotation permitted, assertion forbidden, and forbidden to propagate at all by a host that cannot guarantee the former.

3. Does sensitive: true interact with format? Ruled: no interaction, and now stated rather than implied (requirement 6). The proposed answer was right and the reason is precise: sensitive is a masking directive over values on server-emitted surfaces, while format is a descriptor of the field in the client-submitted definition — a surface sensitive never masks. A format: "email" next to sensitive: true discloses nothing that the already-visible name (recipientEmail) and description do not. Conformance leg A3 pins that the two compose on one variable.

Implementation plan

StepWhereGate
1 ✅schemas/workflow-definition.schema.json — add the property (open string, no enum, per resolved Q1)npm run openwop:check green
2 ✅No schema change needed — chain parameters is additionalProperties: true, so format was already legal. Landed instead as the deferred-mode propagation MUST in workflow-chain-packs.md §"Deferred-parameter expansion" step 1 (resolved Q2).schema check green
3 ✅workflow-variable-format.test.ts — 4 always-on corpus legs + 2 capability-gated host legs; suite 1.68.2 → 1.69.04/4 corpus legs green; host legs gate on workflowChainPacks.deferredParameters
4 ✅Reference host (openwop-app): propagate format chain-param → WorkflowVariable (openwop-app#3131), disambiguated from the provider-hint WorkflowVariable.format; NOT copied into configurableSchemanpm run ci
5 ✅Witness the scenario non-vacuously under OPENWOP_REQUIRE_BEHAVIOR=truedone 2026-08-11 (see §"Acceptance witness")Draft → Active → Accepted

Status advances to Accepted only after step 5, per the RFC 0134 precedent — the reference host must implement and witness before the wire claim is honest.

Acceptance witness

Witnessed 2026-08-11 on the openwop-app reference host (@openwop/openwop-conformance@1.72.2, OPENWOP_REQUIRE_BEHAVIOR=true): the §B host legs of workflow-variable-format.test.ts ran green and non-vacuously, both sabotage-proven.

  • B1 (req 7 — deferred-mode format propagation). The host mints a chain parameter's declared format onto the materialized WorkflowVariable; the leg drives the RFC 0124 deferred-expand seam (openwop-app#3133 extended it to return the minted variables[]) and asserts format present iff minted — emailemail, an unrecognised value copied verbatim (req 2), a non-string param omitting it (req 1). Sabotage: disabling the step-4 format copy in the host's chain loader reds B1 alone (expected undefined to be 'email'), leaving A + B2 green.
  • B2 (req 3 — format is advisory, not validating). The leg runs the vendored fixture conformance-workflow-variable-format-advisory (a variable declaring format:"email" with an off-format defaultValue) via POST /v1/runs — the portable pre-registered pattern, since registration is "POST /v1/workflows or equivalent" (capabilities.md) and the create path is not a mandated portable surface. The run reaches completed; a value/format mismatch does not fail it. Sabotage: wiring a format assertion into the host's run-variable seeding reds B2 alone (expected [200,201] to include 500), leaving A + B1 green.

The propagation MUST (WorkflowVariable.format copied from the chain parameter, not into configurableSchema) is a spec-MUST the reference host had been silently violating before #3131 — the witness is the honest close, not a rubber-stamp. Full suite 2529/0. The §B legs began as advert-tautologies (expect(deferred).toBe(true)) and B2 later passed vacuously by soft-skipping a non-mandated endpoint; each was caught by running-and-proving (sabotage + a run-log check), never by a green. Cited: openwop-app#3131 (propagation) + #3133 (seam) + conformance #931 (B1 drive) / #932 (B2 fixture) + this run.