FS FormScript

The output document

A form produces a document. The output block says what that document contains.

forms
form loan version = "1.0" {
  field amount  { type = number }
  field term    { type = integer }
  field monthly { type = number value = pmt(0.05 / 12, term, amount) }

  output {
    amount: number
    term: integer
    monthly: number
  }
}

The output block is a schema

Each entry names a path and the type of the value at that path:

forms-fragment
output {
  applicant.first_name: string
  applicant.income: number
  dependants: array of object { name: string, age: integer }
  employment: enum("employed", "self")
}

Notice what is not there: you do not write amount: amount. You write the path and the type, and the runtime projects the data plane onto it.

That is what the specification means by an output schema. The output block is a promise about the shape of the document, and the runtime checks that the promise is kept — a field that produces a string where the schema says number is an error, not a surprise at the far end.

The output document is JSON

The engine produces a value tree, and the serialiser writes it as JSON. Answer the loan form above with amount=25000 and term=60:

json
{ "amount": "25000", "term": "60", "monthly": "471.780841100273321124203029223732" }

One detail about it is deliberate and worth knowing.

The numbers are JSON strings. A JSON number cannot carry a canonical decimal without passing through binary floating point, and the specification says a serialiser that cannot do that must emit a string. So it does. The conformance rows check it, and the corpus is full of real forms read back through it.

And the payment is not rounded. A number is a decimal128 — 34 significant digits — and the arithmetic on it is exact to the end of those digits, so the payment of £25,000 over sixty months at 5% a year is all thirty-four of them and not "£471.78". Rounding it is the consumer's decision, not the form's.

Four ways a field can be missing

This is the part that matters most in practice.

The field is In the output document
answered the value
answered, and deliberately empty null
inapplicable absent — the key is not there at all
never answered null

So:

  • A field that does not apply — its when is false — is absent. The key is not in the document. Not null: absent.
  • A field that applies but has no answer is null.
  • A field that applies, has an answer, and the answer is empty is also null.

The difference between absent and null is the difference between "this form did not ask" and "this form asked and the answer was nothing". A consumer that treats them the same will show a guarantor field that was never applicable.

A worked example

forms
form loan version = "1.0" {
  field loan_amount    { type = number }
  field guarantor_name { type = string when = loan_amount > 10000 }
  field notes          { type = string }
  field monthly        { type = number value = pmt(0.05 / 12, 60, loan_amount) }

  output {
    loan_amount: number
    guarantor_name: string
    notes: string
    monthly: number
  }
}

Answer the form with loan_amount=5000:

json
{ "loan_amount": "5000", "notes": null, "monthly": "94.35616822005466422484060584474639" }

There is no guarantor_name key at all: the question was not asked, because the amount is not over ten thousand.

Answer it with loan_amount=25000 instead, and the key appears — as null until the guarantor is named:

json
{ "loan_amount": "25000", "guarantor_name": null, "notes": null, "monthly": "471.780841100273321124203029223732" }

Reading the output in the language

The output document is readable from inside the form, under $form.output:

forms-fragment
field summary { type = string value = $form.output.monthly > 0 ? "has a payment" : "no payment" }

And $form.valid says whether the form is valid — which is what a submit action guards on:

forms-fragment
action submit {
  method = "POST"
  url    = $env.api + "/applications"
  when   = $form.valid
  body   = $form.output
}

That is section 17 territory, but it is worth seeing here, because it is the end of the road: the document says what the form asks, the rules say what makes an answer valid, and the output block says what comes out.