Declare a field Dynamic, describe its real shape as FieldSpec data your code builds at run time, and read the result back narrowed or as it is.
When you need this
Section titled “When you need this”Some fields can’t get a type when you write the flow: what a marketplace’s intake form asks for, or
exactly which fields a case record needs, depends on data you only have once the flow is running.
Declare that field Dynamic and hand AQVEN the real shape as data instead — computed however you
want, read back once you actually know what it is.
- Give the field
type: "Dynamic". On an inference’s ownout, that’s what tells AQVEN the model’s output schema isn’t fixed in the YAML — it gets built from data instead. - Every Dynamic output needs
limits:max_fields,max_depth(1 to 3),max_text_length, andmax_items, whatever shape turns up at run time.aqven checkrejects a Dynamic output missing any of them. - An inference’s Dynamic output also needs
schema_from: a reference to one of its owninfields, typedFieldSpec[]with amaxItemsbound. That field’s actual value at run time — usually computed by an earliercodenode — is the shape AQVEN builds the model’s output schema from.checkrejectsschema_frompointing anywhere else. - Describe the shape with
FieldSpec:name,type,description, and whatever constraint the type takes —maxLength,pattern, orenumforText,minimum/maximumfor a number,maxItemsfor a list, a nestedfieldslist whentypeis"Record". It’s the same grammar a YAML field declaration uses, just written as data. - Add
value_typeon the Dynamic output when you already expect the value to end up matching one particular record or union type in your project once it arrives. It’s a hint, not a check — leave it unset when you genuinely don’t know, or don’t care, which type it lands on. - A node that only carries an existing Dynamic value onward — a
loop,map, orparallelnode’s ownout— can repeatvalue_type, but neverschema_fromorlimits: those belong to whichever inference actually produced the value. - Downstream, nothing reads named fields off a
Dynamicvalue directly. Take it in asDynamicyourself and you get back aDynamicValue—.valuethe raw value,.fieldstheFieldSpecs it was shaped from,.schema_hasha hash of that shape — for code that works by field name without needing a fixed type. Or narrow it to a real record or union type once you know exactly which one it has to be.
Example
Section titled “Example”This is the showcase project’s case_form and record__extract nodes, in its support_case flow.
case_form decides a case record’s form from its intent — a warranty defect, a delivery problem, or a
plain question — before the record itself gets filled in. Create it yourself with:
aqven new my_project --template showcaseHere’s the real example. case_form.node.yaml is an ordinary code node: one CaseIntent input, one FieldSpec[]
output:
apiVersion: "aqven/v1"kind: "Node"node: "code"description: "Builds the case record's form by intent: fields match one CaseRecord variant"run: "case_form"in:- name: "intent" type: "CaseIntent" description: "The case's intent" from: "$intent.out.intent"out:- name: "fields" type: "FieldSpec[]" description: "The case record's form fields" maxItems: 30case_form.py builds that list by intent. The real file has one branch per intent — defect,
delivery, question — each matching one variant of CaseRecord, the union type
How to narrow a dynamic value to a type narrows this same record down to
later. Here’s the question branch; defect and delivery build their own FieldSpec list the
same way:
from aqven.spec import FieldSpecfrom __package__.types import CaseIntent, SupportCaseCaseFormOut
QUESTION_FIELDS = ( FieldSpec(name="topic", type="Text", description="The customer's question topic", maxLength=200), FieldSpec( name="order_id", type="OrderId?", description="The Lumen order number; null if the question isn't about one" ),)
def case_form(intent: CaseIntent) -> SupportCaseCaseFormOut: kind = FieldSpec(name="kind", type="Text", description="The case's kind", enum=[intent]) return SupportCaseCaseFormOut(fields=[kind, *QUESTION_FIELDS])This branch — like the other two — starts with a kind field pinned to the current intent by its
own enum, the same name CaseRecord’s discriminator uses, so the value this shape eventually
produces already carries the tag narrow checks against.
extract, the record loop’s first body node (see How to repeat a step with a limit
for what a loop node’s body does), reads case_form’s output as its own form_fields input and fills in
the record against it. Its inference file is where the field that doesn’t have a fixed shape gets
declared:
apiVersion: "aqven/v1"kind: "Inference"description: "Fills in the case record's form from the text, summary, and attachments; on a repeat, applies the review notes"in:- name: "form_fields" type: "FieldSpec[]" description: "The case record's form fields for this case's intent" maxItems: 30out:- name: "record" type: "Dynamic" description: "The case record filled in from the form" schema_from: "$in.form_fields" value_type: "CaseRecord" limits: max_fields: 30 max_depth: 2 max_text_length: 400 max_items: 10The real inference also takes message, summary, feedback, photo, and invoice as input — none
of them feed the Dynamic field, so they’re left out here.
schema_from: "$in.form_fields" tells AQVEN to build record’s actual output schema from whatever
form_fields turns out to be on this run — one shape for a defect case, a different one for a delivery
case, without writing three separate inferences. limits bounds it regardless of which shape shows up:
at most 30 fields, 2 levels deep, 400 characters per text field, 10 items in any list.
value_type: "CaseRecord" records the expectation that whichever shape comes back will fit one of
CaseRecord’s three variants — narrow is what actually checks that later, not this field.
What happens to record next — read as Dynamic by field name, or narrowed to CaseRecord once the
code knows exactly which variant it is — is covered in the pages linked below.
See also
Section titled “See also”- How to narrow a dynamic value to a type — the other half of the
Dynamicstory: turning a value likerecordinto an ordinary typed value once you know exactly what it is. - How to repeat a step with a limit — what a
loopnode’s body and passes are, the shapeextractandvalidaterun inside. - How to write a step in Python —
case_form, the node that builds theFieldSpec[]shape this page’s example is built from. - The engineering loop — what to do when a run’s output isn’t what you expected.
- Fields and bindings — every field on
InputField,OutputField, andBoundField, generated from the code. - Media and dynamic values — every field on
FieldSpec,DynamicLimits, andDynamicValue, generated from the code. - Type specifications — how a discriminated union like
CaseRecordis declared.