The iframe channel: three decisions

July 30, 2026 · View on GitHub

Why closeOnResult defaults to off, why error is not about validation, and why the payload of result is the envelope of /invoke again. The contract — the four kinds, their payloads, the rule about v — is in sdk.md.

closeOnResult is off by default

The obvious default is the wrong one. A modal that closes when the run finishes reads well for create task, whose result is one line of confirmation, and destroys the feature for everything else: an image, a table, a download link and a printed log are all drawn inside the page, and closing the overlay throws away exactly what the user opened it to see. There is no way for the library to tell the two cases apart — the outputs arrive after the modal is already open, and the decision has to be made before it.

So the default is the one that loses nothing: the modal stays, the host is told, and whoever knows their function returns a confirmation asks for the autoclose. It also keeps the release additive: a modal built before this existed behaves after it exactly as it did, because closeOnResult: false is the old behaviour.

error means a run failed, not a field

The page has two very different kinds of bad news. A field the browser rejects as you type —a Min, a Pattern, a required value still empty— never reaches the server, and a field the server rejects arrives as a 422. It would be easy to send both as error, and the result would be a host that cannot act on either: a message that may mean "your data is wrong, keep typing" or may mean "the call failed" is a message you can only log.

The line drawn is the envelope's: error is emitted from the error of a /invoke answer and from nowhere else, so it always means a run happened and did not produce outputs. Client-side validation is silent, and so is a malformed envelope or a request that never arrived — the page shows those where they belong, in its own error block. This is the same separation the HTTP layer already makes between a contract violation and an exception (http.md): one kind, one meaning, and a host that can branch on it.

navigate exists for the same reason. The OpenForm branch ends with the iframe moving to another form, which is neither a result nor a failure; folding it into result would make completed true for a run that produced nothing, and the host would refresh a list that did not change.

The payload is the envelope again

result carries the same array the page has just finished drawing with — the result of the /invoke envelope, normalized to a list, untouched. Not a summary, not a flattened value, not a new shape.

The reason is that a second shape is a second thing to keep true. The outputs format is already published (outputs.md), already what call() resolves to, and already what the page renders; a host that has call() working can move to a modal without learning anything new, and a new output type reaches the channel the day it reaches the renderer, with nothing to update here. A purpose-built payload would have been shorter to read and would have started drifting the first time an output type gained a field.

The same argument settles results in closed: it is the outputs of the last run, because each result replaces the previous one. A list of every run would be a new shape with no reader — a host that wants each run as it happens already has onResult.

Related: sdk.md, outputs.md, open-form.md, security.md.