Skip to content

Types and Empty Values

What type does a field need, and what happens when its value is missing? A type describes a value and its available operations. An empty value has a known type. If a node’s output is unresolved, downstream expressions do not yet have a usable contract.

TypeInvoice exampleMeaning
text"INV-104"Text, including identifiers.
number120A numeric amount.
booleanfalseA yes/no review decision.
date"2026-09-07T14:00:00Z"Date/time; ISO text carries it across JSON.
fileUploaded invoice PDFMetadata: fileName, mimeType, sizeBytes, url.
custom.invoiceInvoice ID, Amount, StatusStructured fields without stored identity.
dataRecord.custom.invoiceSaved invoiceInvoice fields plus id, createdAt, updatedAt.
list.custom.invoiceExtracted invoicesA list of Invoice shapes.
list.dataRecord.custom.invoiceSaved invoicesA list of records with identity.

An Ask AI result can have the Invoice shape without being stored. Create a record to obtain identity. Update data and Delete data need a saved record. Ask AI rejects dataRecord.* response types. A returned record can satisfy its corresponding shape contract. Adding dataRecord. to a type name creates no row.

In a stored schema, a custom-typed field references another record. For direct writes, supply its ID. Expression field access resolves the reference. An ordinary shape’s structured field can hold nested data instead. Stored timestamps use expression IDs createdAt and updatedAt, displayed as created time and updated time. They are text, so convert them before date operations.

In the app: choose stored identity with the type selector’s Expect data record with id checkbox. Choose a list with List. For extracted invoice fields, leave the record checkbox off. With an assistant: when defining inputs, use the exact type ID to make these choices.

Resolve unknown outputs before using their fields

Section titled “Resolve unknown outputs before using their fields”

An unresolved output blocks field access. Select the node’s supported output type, run it with representative data, or declare a documented sample as unverified. Observed types belong to that node, not every instance of its action.

In the app: inspect Action output format. In an editable development view only, No output format yet offers Test action. In a read-only view, it directs you to run in development. Configure the action before testing. Testing makes its real external request.

With an assistant: use <workflowId> and <nodeId> discovered in the node guide. Every scoped command here supplies --workflow, so no CLI pin is assumed. Inspect the node’s status and resolve its schema to see its output type.

Terminal window
cai node status "<nodeId>" --workflow "<workflowId>" --json
cai schema resolve "node.<nodeId>" --workflow "<workflowId>" --json

MCP uses node_status with nodeId and schema_type_resolve with typeId: "node.<nodeId>". For scoped calls, supply workflowId: "<workflowId>" as a string containing a positive integer.

schema resolve only inspects. For a documented sample, save the provider’s response object or nonempty array of objects as sample.json. Declare the sample and resolve the schema to inspect its type.

Terminal window
cai node declare-output "<nodeId>" --workflow "<workflowId>" --sample-file sample.json --json
cai schema resolve "node.<nodeId>" --workflow "<workflowId>" --json

MCP uses node_output_type_declare with nodeId and the parsed JSON in sample.

Alternatively, inspect the node’s test inputs to get the IDs for your representative values.

Terminal window
cai run inputs "<nodeId>" --workflow "<workflowId>" --json

Save representative values under the returned IDs in node-inputs.json, including every data.flowInputs[].id. Then test the node and resolve its schema to inspect the output type.

Terminal window
cai run node "<nodeId>" --workflow "<workflowId>" --inputs node-inputs.json --json
cai schema resolve "node.<nodeId>" --workflow "<workflowId>" --json

For MCP, use run_inputs with nodeIdOrFlowId. Then use run_node with nodeId and parsed inputs.

After a representative run completes, the node can adopt its observed output. If a known Invoice record is absent at runtime, Get data returns null. Define the missing-record branch instead of inventing a schema.

For text, number, or boolean values only, use ?? with a literal fallback of the same type. These expressions supply fallbacks while preserving real 0 and false.

flow.amount ?? 0
flow.reviewNeeded ?? false
flow.invoiceId ?? "missing"

Dates, shapes, records, and lists reject ??. For these types, branch on presence instead. For output wrappers, access .data first. Unless you also flag an unknown invoice amount for review, do not default it to zero.

Text .isEmpty() recognizes null, undefined, and "". It treats whitespace as present. Text fallback, however, replaces whitespace-only text. Outside list predicates, presence checks for shapes, records, and files treat only null and undefined as missing. They treat {} as present. Inside .filter() and .every() field predicates, {} and [] count as empty. Check the fields the next step needs.

In list predicates, row.status === "" matches empty text. The predicate row.status !== "" also passes missing values. The predicate row.status.includes("") matches every text value. Use presence checks to exclude missing values. When a schema is known, an unknown field fails compilation. At runtime, .filter() drops missing-field constraints and can retain every row. For missing-field constraints, .every() returns false. Revalidate expressions after schema changes.

In development flow tests, if you omit a used input key, the run reuses its saved test value. To test missing data, supply its exact ID with null. Manual Test converts null text to "" and other missing values to null. Outside manual Test, omitted optional inputs become null. Check the intended trigger/live path too.

Distinguish a missing list from an empty list

Section titled “Distinguish a missing list from an empty list”

On a typed null list, both .isEmpty() and .isNotEmpty() return false. Field filtering can fail on that list. Require the input or make its producer return []. Test null separately. For scalar values, .toList() wraps null as [null].

Operation on []ResultApplicable list
.length, .sum()0Any list for length; numbers for sum.
.average()nullNumbers.
.first(), .last(), .item(n)nullAny list; no position exists.
.find(predicate)nullStructured items with a valid predicate.
.some(predicate)falseStructured items with a valid predicate.
.every(predicate)trueStructured items with a valid predicate.
.join(separator), .formatAsText(...)""Lists supporting the operation.
.stringify()"[]"Any list.

If at least one row must pass, require a positive length as well as .every(...). An empty list satisfies a valid every predicate.

A date-typed field supports date operations directly. For text, use .toDate(). By default, parsing uses strict ISO. Invalid input returns null. Use the returned bindings and explicit epoch units to convert text into dates.

flow.dateText.toDate().format("YYYY-MM-DD")
flow.epochText.toDate("epoch_seconds")
flow.epochText.toDate("epoch_millis")

Epoch seconds are multiplied by 1000. Epoch milliseconds and numeric values used directly as dates already use milliseconds. Parsing and formatting default to UTC. If parsed text includes an offset or Z, that takes precedence. .format() uses Moment tokens, so YYYY-MM-DD returns year-month-day text.

See expression operations for each operation’s input, result, and spelling.