Open source · for public-sector and regulated forms
Official forms, owned by the people who write them.
Write a form in a visual designer, in Excel or in YAML. quillflow turns it into an accessible web form with live lookups in your registers, and checks every submission on your server with exactly the same rules.
Open the designer Read the documentation
- 1 form definition
- 2 runtimes, TypeScript and .NET, held to one conformance suite
- 0 lines of front-end code per form
Runs in your browser with a pretend address register. See its source.
Why quillflow
Forms are where people meet public services. They change often, and they must be right, accessible and secure. quillflow puts the people who own them in charge and takes care of the rest.
-
Written by the people who own the form
Caseworkers use the designer or Excel, developers use YAML. All three open and save the same files. Problems are reported in the author's words:
row 14, column relevant: Unknown field "incme". Did you mean "income"?
-
The same rules in the browser and on the server
Every submission is checked again by the same engine. Hidden answers are dropped and calculations recomputed. The TypeScript and .NET engines pass one shared conformance suite, so they cannot drift apart.
-
Live lookups, no secrets in the browser
A company from its CVR number, properties at a postcode, an address as you type. The form names the lookup, and your server calls the register with its own credentials and the signed-in user.
-
Accessible by default
Real labels and fieldsets, an error summary that links to each field, focus management, a WAI-ARIA combobox for search, and numbers typed the local way. Patterns from GOV.UK and Det Fælles Designsystem.
-
Embed it anywhere
One web component for plain HTML, Blazor, React, Angular or Vue. No shadow DOM: your design system styles it. Server SDKs for ASP.NET Core and Node.
-
Rules anyone can read
Rules are Excel formulas:
AND(AGE(birthdate) >= 18, amount <= @valuation.value * 0.8). Text can include answers, with numbers written the local way:{{ amount | number }} kr.
One form, written the way you work
The same question in each format. Whichever you write, the compiler produces the same definition.
| type | name | label::da | required | relevant | constraint |
|---|---|---|---|---|---|
| select_one employment | employment | Beskæftigelse | yes | ||
| text | cvr | CVR-nummer | yes | employment = "self" | MATCHES(., "^[0-9]{8}$") |
| decimal | income | Årlig indkomst (kr.) | yes | employment <> "unemployed" | . > 0 |
- name: cvr
type: text
label: { da: CVR-nummer, en: CVR number }
relevant: employment = "self"
required: yes
constraint: 'MATCHES(., "^[0-9]{8}$")'QuestionCVR-nummer
Answer typeText ▾
Must be answeredYes ▾
ShownOnly when … ▾
Shown whenemployment = "self"
RuleMATCHES(., "^[0-9]{8}$")
Open the designer and choose Open example → Loan application to see it for real.
What runtimes read. Nobody writes it by hand.
{
"kind": "field", "path": "applicant.cvr", "type": "text",
"relevant": {
"src": "employment = \"self\"",
"ast": { "fn": "EQ", "args": [{ "var": "applicant.employment" }, { "lit": "self" }] }
}
}How it fits together
A few lines in the stack you already have
<script type="module" src="quillflow.bundle.js"></script>
<qf-form form="change-of-address" api="/api"></qf-form>@rendermode @(new InteractiveServerRenderMode(prerender: false))
<QuillflowForm Form="change-of-address" OnSubmitted="@(e => receipt = e.Reference)" />builder.Services.AddQuillflow()
.AddFormsFromDirectory("forms")
.AddConnector("address_search", (p, ctx, ct) => addresses.SearchAsync((string)p["query"]!, ct))
.OnSubmit((submission, ctx, ct) => cases.CreateAsync(submission, ct));
app.MapQuillflow("/api").RequireAuthorization();const handler = createHandler({
forms: memoryFormStore(definitions),
connectors: { address_search: ({ query }) => addresses.search(String(query)) },
onSubmit: (submission) => cases.create(submission.data),
});Something for everyone around a form
Form owners
Change a question, a rule or a message yourself, see it live, and hand over a file that passes review. The designer · Authoring
Developers
Embed one element, register a connector per lookup, handle a clean submission. No per-form code. Embedding · Servers
Architects and security
Server-side validation by the same engine, allow-listed lookups, no secrets in the browser, strict CSP. Security · Accessibility
See for yourself
Build a form in the designer, or write one in the playground. Everything runs in your browser.