Models: parsing and breaking down data
Pipelines often need to work with structured data — a webhook payload, an API response, a file. Flovello represents that structured data as a model: a named schema you define (an object with fields, or an enum), which flows through your pipeline as a single typed value. This tutorial covers the handful of nodes that create, split apart, and serialise models.
What's a model?
A model is a schema you create under Models in the app — a named set of typed fields, which can nest objects and arrays. Here's the Fields editor on an "Order" model's page once defined, with a customerId, a list of line items, and a total:
Once defined, a model behaves like any other data type: pins can carry a single model value between nodes, typed to that specific model. The rest of this tutorial uses this "Order" model as its running example.
What happens when a field is missing
The dropdown beside each field's type is the one decision that shapes how the rest of your pipeline has to be built. It answers two questions at once, which is why every option names both.
Nullable or non-nullable — is null a value this field accepts at all? A non-nullable field is a promise: it can never hold null, so nothing downstream has to defend against one.
Required, or what it defaults to — what happens when the incoming data doesn't supply the field?
- Required — parsing fails with an error naming the field. For data you cannot proceed without, like
customerIdabove. - Default null — the field becomes null. Only available on a nullable field, since filling null while refusing null is a contradiction.
- Default empty — for arrays and objects: an empty list, or an object whose own fields are filled by their own rules, all the way down. An
itemsarray that defaults to empty means the loop over it runs zero times instead of needing a guard. - Default value — for text, numbers, booleans and enum references: the literal you enter.
quantityabove defaults to1, so arithmetic downstream never has to cope with a missing number.
The nullability half matters because omitting a key and explicitly sending null are two different messages from whoever is calling you:
{ } (key absent) |
{ "quantity": null } |
|
|---|---|---|
Non-nullable, default 1 |
1 |
1 — null isn't accepted, so the default answers |
Nullable, default 1 |
1 |
null — the sender meant it, so it's kept |
| Nullable, required | fails | null |
| Non-nullable, required | fails | fails |
Choosing non-nullable is what buys the guarantee. Reach for nullable when data is genuinely, meaningfully absent — every pin carrying a nullable value is marked with a ? in the editor, at both ends of every connection, so you can see exactly how far a possible null travels, and Is null? and Default if null are there to handle it.
The default also applies when you build a value with Make rather than parse one. A Make pin you leave untouched shows what the model will fill in greyed text — null, [], 1 — instead of a value it isn't going to produce. Click it to set something else, or clear it to go back to the model's default. A required field is the one case Make can't fill for you, so leaving its pin empty raises a warning on the node.
Parsing raw input into a model
Raw input — a request body, a file, an API response — usually arrives as a stream of bytes, not a model. The Parse to model node bridges that gap: pick a model in the inspector and a wire format (JSON, YAML, XML, CSV, or Excel), and it parses the stream into a typed value of that model. Because parsing can fail on malformed input, Parse to model sits on the execution flow with two branches — success and error — so you can handle bad input explicitly. When the input is a list rather than a single object, an array variant parses the stream into an array of the chosen model instead.
Breaking a model into fields
Once you have a model value, Break splits it apart. Pick the model in the inspector and the node grows one output pin per field, each typed to match — a string field becomes a string pin, a nested model field becomes another model pin you can Break again. Break is pure: it has no execution pins, and its outputs are computed lazily, only when something downstream reads them.
Composing a model from fields
Make is Break's counterpart: pick a model in the inspector and it grows one input pin per field, then emits the assembled model on its value output. Make is also pure — the model is assembled lazily, only when something reads it. Use it whenever a downstream node expects a whole model rather than loose fields, such as before building a request body.
Serialising a model back out
To turn a model back into text or bytes, use Model to string or Model to stream. Both are pure. Model to stream supports JSON, YAML, XML, CSV, and Excel; Model to string supports all but Excel, because an .xlsx file is a zip archive rather than text. Both take a single model or a list of them — a list becomes a JSON array, an XML container element, or the rows of a CSV file or workbook. Reach for Model to string when you just need the text — for example to log a model with Log message. Reach for Model to stream when the destination is a streaming consumer, such as the body input of an HTTP request — it writes the serialised model straight to the socket without buffering the whole payload as a string first.
Spreadsheets and CSV: mapping a tree onto a grid
A model is a tree; a spreadsheet is a grid. The four other formats can carry a tree directly, so they need almost no configuration. CSV and Excel cannot, so how a model projects onto rows and columns is declared on the model itself, under Hints — model-level settings on the Hints tab, per-field settings behind the lightbulb on each schema row.
The projection has four cases, one per shape a field can have:
| Field | Becomes |
|---|---|
| A scalar | One column, named by the field's external name |
| A nested object or model reference | Its own fields as columns, sharing a prefix — address.city by default |
| A list of scalars | One column, split on an item separator you choose |
| A list of models | Rows of its own |
That last case is the interesting one, and it is what a real order file needs. A list of models can get its rows two ways:
- From another sheet (Excel only). Give the field a sheet name and a key column, and each parent is matched to the child rows carrying the same key. An
Orderssheet and anOrderLinessheet joined onorderNois the classic supplier workbook. - From this same table, grouped by a key column. A flat export that repeats the order columns on every line row is one model per order, not per row. Give the field a parent key column and nothing else, and rows sharing that value collapse into one parent carrying one item each. This is the only option for CSV, which is a single table by definition.
A few things worth knowing before you point a pipeline at a supplier's file:
- A header row is always required. Without one there is nothing to map a column onto a field by. If a sender really has no header, put a header line in front of the data first.
- A blank cell means absent, not empty. A table cannot tell the two apart, so a blank cell lets the field's declared default apply — the same as a column the sender left out entirely.
- The separator and character encoding of a CSV file are detected unless you declare them. A semicolon-separated, ISO-8859-1 export out of a Windows tool is read correctly without configuration.
- Cells are read by the field's declared type. A
STRINGfield gets what the cell displays, so a date column reads as a date rather than as the serial number underneath it; anINTEGERorDECIMALfield gets what the cell stores, so a price is never rounded to whatever its display format shows. Writing works the same way in reverse, which is why an article number like0012345in aSTRINGfield keeps its leading zeros. - Only
.xlsxand.xlsmare readable. The legacy binary.xlsis a different format entirely and has to be re-saved first.
Worked example: reading fields from an order webhook
Here's what it looks like to receive an order webhook, parse its body into an "Order" model, and break that model apart into individual fields:
From there, wire customerId into a Log message node. You don't need to wire anything to the Error branch: an unwired execution output already fails the pipeline automatically, so malformed webhooks are surfaced by default. Only wire a Fail node there if you want a more specific failure message than the default.
See also
- Node reference: Models category for the full list of model-related nodes.
- How execution flow works for more on pure nodes and lazy evaluation, which is how Break and Make behave.