Skip to main content
Your application owns inputs, labels, validation messages, focus and layout. Bijection owns editable drafts, file transfers, validation results and one retained submission. The SDK supplies no form component or stylesheet and requires no UI library. The dashboard builds its automatic operation form on this same public API, through createOperationClient. A form loads its file upload code only when a file is selected, so a bundler that splits dynamic imports keeps that code out of the initial load. Every snapshot has an error, which says why its owner’s latest command or background step failed. Owners that do something over time, namely operation calls, forms, file selections, upload entries and upload inputs, also report what is happening as state, which never carries an error. A field holds a value instead: its snapshot reports the value, its issues and error, and a React field binding exposes that snapshot as snapshot. Every command returns a promise that is already handled: a failure appears on that owner’s error, so onClick={() => void form.recover()} needs no wrapper, and awaiting the promise still observes the rejection. No command throws synchronously. Keep application state only for your own work, such as refreshing a list or navigating.

The names you need

Reusable controls accept OperationField<T> for a typed field, AnyOperationField for any field useOperationField can bind, OperationUploadField for any field that selects files, and AnyOperationForm for a form of any operation. The other form types in the React and browser references name the parts of these.

Create a form

Use useOperationForm to retain editable values, list identities and file selections, then freeze one exact request on Save:
field.choose(files) validates the whole selection before preparing any file. The example illustrates commands. In a component, subscribe to the fields you render:
The reference application’s TextField, AttachmentFields, and SaveControls are application components, not SDK exports. Their implementations live together in scripts/fixtures/expense-reports/src. TextField takes a string field, a label and an optional id. AttachmentFields takes a list field and a label, and renders its file input, selected files and each item’s caption. SaveControls takes the form, an optional label, disabled, onSubmit and onStartAnother; without onSubmit, Save submits the surrounding native form. Its form is an AnyOperationForm, which accepts the form of any operation with its snapshot, commands and selections but without typed field navigation. Each selected file renders as an OperationFile, which takes an owner (the form or a field) and a selectionKey. The file input, OperationUpload, takes an OperationUploadField: any field that selects files, whatever its value shape. A host that finds fields at runtime narrows one with isOperationUploadField(field). A complete composition, using those helpers and the generated operation, is:
Keep recovery and pending file removals outside the disabled editable fieldset. SaveControls observes the same form, shows its error and calls its commands; it does not maintain another submission state. onAccepted owns successful presentation for both Save and recovery. If onSaved fails, the operation remains accepted and the application shows the presentation error. Its asynchronous work is not part of the form command’s completion. An application that disables other actions during refresh must retain that busy state until the refresh finishes. Restoring a retained request does not replay presentation; checking its current result can confirm acceptance again.

Bind controls

The binding generates an ID shared by the input and its issue description. To choose one yourself, use useOperationField(form.field("title"), { id: "report-title" }). Use binding.id for the label; prop getters keep that same ID. Call the hook once per mounted control, including when two controls edit the same field. useOperationField returns the field’s commands, its subscribed snapshot, and native prop getters where the declared field supports them. getInputProps() binds text, checkbox and file inputs; getTextareaProps() binds editable text; getSelectProps() binds string values, including literal choices. Text bindings preserve raw text; checkbox bindings use checked; upload bindings use file selection, never a controlled value. Custom controls can call edit, set, choose and touch directly; like the bindings’ handlers, their failures appear on snapshot.error. Reusable controls can name each part: a field’s OperationFieldCommands<T> and OperationFieldSnapshot<T>, which is what every field reports (OperationFieldSnapshotBase<T>) plus what its value’s shape adds, such as OperationUploadSnapshot; and a binding’s OperationNativeBindings<F>, with OperationInputOptions, OperationInputProps, OperationTextareaProps and OperationSelectProps. useOperationField accepts any AnyOperationField. field.set(value) assigns an exact typed value; field.edit(text) retains unfinished text such as "-" for an integer. field.advanced.editJson(text) is the explicit advanced editor and uses the public JSON value encoding. Snapshots expose the parsed value only when valid, plus issues and editability. Text-editable fields expose unfinished text; optional fields expose inclusion; union fields expose their selected alternative. Only upload fields expose file selections and usable limits (multiple, capacity, maxBytes, contentTypes). Selections expose filenames, state with progress, error and the actions available now, without controller entries or attachment declarations. A selection’s path follows its current argument position; selectionKey remains stable when its row moves. localFile is available for an application-owned preview only while this page has the original bytes. It is never persisted. Pass application handlers, refs and accessibility descriptions to the getter so they compose with the binding. Binding handlers run before application handlers, descriptions are combined, and disabled: false cannot enable a frozen field. Values, checkbox state and file multiplicity remain owned by the field:
The composed ref stays stable across field edits and parent renders when the supplied ref and focus function are unchanged. A replaced ref or focus function detaches the old binding and attaches the new one. Object refs and callback-ref cleanup are supported. For a value-oriented widget, getValueProps() supplies its typed value and onValueChange(value) callback. The callback returns void; synchronous validation failures and rejected writes remain on snapshot.error. Read value as possibly undefined while input is incomplete. A nullable field can be cleared with null; only an optional field accepts undefined. The getter performs no value conversion. Its change callback stays stable while the field handle is unchanged. Native change and blur handlers also stay stable when the supplied application handlers are unchanged. Values, issues and editability still follow the current snapshot; retaining a callback never bypasses a frozen field’s command checks. Native bindings follow the declared shape and available commands, including while input is incomplete: Native changes still pass through field validation. A literal true refuses false. Nullable booleans and structured values use getValueProps() with a widget that represents every declared value. Read-only projections expose no mutation bindings.
Combobox is an application widget: it forwards controlProps to its focusable anchor, calls onValueChange with one declared value, and calls its onBlur when focus leaves the complete widget, including its popup. A widget with a different value model adapts it explicitly. For example, a date picker returning Date | null can map a UTC calendar date to a nullable ISO date string:
For unfinished text, use edit instead; it preserves input such as "-" without inventing a parsed value. Call and await set or edit directly when subsequent work depends on a successful write. Spread getControlProps() on the widget’s anchor. It supplies editability, focus registration and issue associations, preserving supplied event handlers. Native input, textarea and select getters automatically touch on blur. Custom anchors leave interaction completion to their widget. An explicit focus function can direct validation focus into an editor or portal; its anchor must be mounted inside the native form. For a popup rendered outside the anchor’s DOM subtree, call touch() when the user leaves the complete widget, according to that widget’s focus lifecycle:

Reusable controls

Build reusable application controls around typed field handles. Each control owns its subscription, markup and messages:
The same composition applies to object fields. For example, an address group can be reused wherever the operation accepts that shape:
An object field or collection item keeps the parent’s draft, validation and submission owner. A group needs no new form instance, provider or repeated validation schema. Keep collection subscriptions in their list component and field subscriptions in the controls; each then updates for the state it renders. Commands follow the declared kind: objects expose field(name), lists expose items(), item(itemKey) and append(...values), and records expose add(name, value) and stable entries with rename(propertyName). Optional fields expose include(boolean); unions expose selectAlternative(index). File selection appears only on a declared upload or a collection with one unambiguous upload route. A plain text field has no choose, move or remove command.

Subscribe to what you render

useOperationForm returns a stable handle without subscribing its component to state. Subscribe where the result is rendered. useOperationSnapshot subscribes to a form, a field or a file selection, with an optional selector:
Selectors compare results with Object.is; unchanged built-in collection snapshots keep their identity. For a derived object or array, pass a comparator explicitly:
A whole-draft editor can explicitly select the entire snapshot. Place useOperationField in the control component so unrelated controls stay quiet.

Acceptance and validation

onAccepted(receipt) observes confirmed acceptance after a mounted form’s submit, retry, recover or retryDraftWrite command releases its task. It runs once per request on that form instance, uses the latest committed callback, and may navigate or call startAnother(). Restoration alone does not invoke it; an explicit recovery after reload can confirm acceptance and notify the newly mounted form. Unmounting or changing the draft suppresses pending notifications. This is a presentation callback, not a durable side-effect handler. Catch callback failures to display them; otherwise they are reported to the console. They cannot reject an accepted submission, change its receipt, or trigger another request. Validation remains accurate from initialization. issues contains all local findings; visibleIssues reveals them after blur or a submission attempt and updates them during editing. touched means the user has left the control; dirty compares its current input with the retained initial input, including unfinished text. Reload preserves that baseline even when the caller supplies new defaults. Discard restores it and clears interaction state. Row interaction follows stable item identities through reordering. Native bindings supply IDs, names, refs, edit availability, blur/change handlers and issue associations. Render the issue element using issueProps as above. handleSubmit prevents native navigation, retains failures, and focuses the first mounted invalid control within that form. submit() is the programmatic command; revealIssues() also supports a preview without dispatch. Applications own markup, labels, styling, custom widgets and message presentation. Advanced editors use form.advanced.editDraft, form.advanced.fieldAt and snapshot.advanced.fieldDraft; field validators and structural paths are under binding.snapshot.advanced in React.

Lists

field.items() returns an immutable collection of stable row handles. Its identity changes only with membership or order. Subscribe to it directly in React:
Each application-owned row subscribes to its own fields; editing a caption does not change the collection. field.item(row.itemKey) finds the same row after reordering; a removed row’s handle cannot edit a later row.

File fields

Selection keys identify uploads independently of row positions. A native file input can select files without a UI dependency:
Render each uploads.snapshot.selections entry using its filename, state, error and actions. A selection’s state is an upload state, loading while its uploads are restored, or failed with operation: "load" when restoring them failed. Its error is its latest failure: a command’s refusal or failure, the file’s check or transfer, or restoring its upload. A command clears its own earlier failure when it starts. Its actions are AttachmentAction values: the file’s own and those for the whole input. form.selection(selectionKey) or field.selection(selectionKey) returns an OperationSelection, a stable handle with getSnapshot(), subscribe(listener) and one command per action kind: pause(), resume(), choose(file), retry(), discard(), retryLoad() and forgetLocalRecovery(). A custom React file row subscribes with useOperationSnapshot(selection) and renders one control per entry in actions, disabled when the action is; the actions table lists typical controls. A choose action’s requirement says whether the original file or another one is needed; pass the chosen bytes to choose(file), and the controller checks the required identity. The form applies its own editability to each action’s disabled. forgetLocalRecovery() explicitly clears unreadable upload recovery; it neither cancels a remote upload nor forgets the form’s Save request. The reference application’s OperationFile renders a selection this way. Removing a row retains its cancellation intent before hiding it. Render the selections whose removing is true, snapshot.selections.filter((s) => s.removing), outside disabled editing controls so a user can retry form.selection(selectionKey).discard(); it stays available while the draft is frozen, unless the form is working. Unresolved removals survive reload and block editing and Save. The application renders recovery with the commands below.

Save and recovery

form.discard() clears editable input back to its initial values and durably reconciles selected files. Pending removals survive reload and remain visible. It is unavailable during an unconfirmed Save. Forget local recovery is a separate advanced action and never claims to cancel remote work. Save retains its exact request before dispatch. While state.kind === "uncertain", keep editing disabled and show Save not confirmed. Check result calls await form.recover() and never submits; Retry save calls await form.retry() with the original arguments, contract and request key. An absent result does not make the draft editable. A confirmed result is state.kind === "accepted". startAnother() releases local selection bookmarks and starts a fresh private creation context; accepted request recovery remains retained. draftKey identifies editable work, contains 1–128 UTF-8 bytes and stays stable across renders and reloads. Keep one client per signed-in account and replace it when that account changes. Initial values initialize each draft identity; later prop changes do not overwrite retained edits. Drafts are encrypted and scoped to the tab, deployment, verified account, operation and draft key. They contain no file bytes or credentials. Each record is limited to 512 KiB, 4,096 tree nodes across its current input, initial baseline and recovery structure, and depth 32, in addition to the operation’s upload limits. Malformed or inaccessible recovery data produces state.kind === "blocked". Keep it and show the cause. A failed editable-draft or request-link write can use retryDraftWrite(). A request-link retry retains the same local recovery link before checking the result; it cannot dispatch until that write succeeds. Explicit forgetLocalRecovery() clears local recovery only; it neither cancels an uncertain Save nor discards remote preparations. Check the saved business records before offering it. Transport errors never authorize a new Save. Throw OperationRefusal from preparation to record a terminal refusal, such as a stale report revision. The engine rolls back business writes and fences that request key. A checked native refusal restores the original editable fields and file selections, including after a lost reply and reload. No application error predicate is needed. Timeouts, temporary access failures and an absent recovery result keep the request frozen.

Other frameworks and advanced editors

For another framework, createOperationForm from bijection/browser exposes these commands with getSnapshot(), subscribe(listener) and start(). Call start() once per mount and its returned cleanup on unmount. Rendering or constructing a controller starts no remote work. The React hook supplies this lifecycle. The dashboard uses these public APIs for its own object and geometry controls; it has no private renderer bridge into the SDK. Advanced schema-driven editors can inspect field.getSnapshot().advanced.validator and advanced.fieldDraft, and call field.advanced.editDraft(update) or form.advanced.editDraft(update) to retain incomplete input exactly. createFieldDraft, fieldDraftFromValue, parseFieldDraft and updateFieldDraftAt from bijection/browser share the draft model and value encoding. form.advanced.fieldAt(identityPath) navigates object names and stable item keys; union alternatives add no path segment. Ordinary application inputs use set, edit, choose and the typed row commands above. initialFieldDraft initializes an advanced editor; fieldShape can refine an any argument for local input. Neither changes the admitted operation contract: submission still validates that contract. initialContext, form.setContext(value) and form.context retain application-owned draft context, such as the revision from which defaults were read. Context grants no authority and never replaces business arguments. All draft edits keep the same retention bounds and file ownership checks; editors cannot fabricate or move selections outside declared upload fields. The expense-report example contains the complete controls for editing, upload recovery, uncertain Save, replacement and removal. The lower-level upload controller remains useful when a host already owns its editable draft and submission lifecycle.