Templates and the form schema
A template is a schema, not a document. Understanding it is most of understanding DocDealer.
A template is not a Word file with holes in it. It is a schema with three parts, and the same schema drives the UI form, the AI generation and the API surface — which is why any client produces the same validated document.
The three parts
Prop
Type
Objects and fields
formSchema holds objects and fields. An object is a repeating group — "the parties to this
document", "the locations being described" — and a field is a single value inside it.
That structure is exactly why formData keys look the way they do:
{
"contactos._count": 2,
"contactos.0.nombre": "Ana Torres",
"contactos.1.nombre": "Luis Gil"
}objectKey._count says how many entries the repeating group has; objectKey.index.fieldKey
addresses one value inside one entry. There is no nesting beyond that — the shape is deliberately
flat so it survives a JSON round-trip through any client.
Read the real keys rather than guessing: GET /api/templates/{templateId} returns formSchema
with objects and fields, and each field's fieldKey (falling back to its id) is what goes
in the key.
Field types
| Type | Value shape |
|---|---|
string | Text. |
number | Numeric. |
date | ISO date string. |
checkbox | Boolean. |
selector | One of a declared set of allowed values. |
file_attachment | An object wrapper — an uploaded file, optionally with an AI analysis of it. |
geoaddress | An object wrapper — a structured address with coordinates. |
connector_tool | An object wrapper — a value fetched from an organization connector. |
The first five are scalars: pass the value directly. The last three are object wrappers, because one field holds several derived values at once — an analysed ID document yields a name, a number and a date, and a geocoded address yields road, house number, postcode, city and coordinates.
Values are checked against the type on every write — a number for number, YYYY-MM-DD for
date, true/false for checkbox, a declared option for selector — and a mismatch fails the
request naming the key. Each object and field type also accepts a fixed set of schema properties;
an unknown property is a validation error rather than something silently dropped. objectKey and
fieldKey match [a-z][a-z0-9_]*. Object ids and keys must be unique among objects;
field ids and keys must be unique within their own object. Different objects may reuse field
names, and an object and a field may share a name.
Sections and example content
The template's content is divided into sections, and each section carries example content: real
prose from real documents of that kind. Generation shows the model the examples plus the relevant
form values and asks for one section. It is imitation against a known shape, not open-ended writing —
which is what makes it fast and cheap. See Generation.
A section can also be marked to skip AI entirely and emit its example content verbatim when
conditions hold. That is a <when result="static"> inside the case, covered in
Rules.
Response language
Every AI prompt appends a "respond in X" directive taken from the template, never from the
reader's UI language. The chain is template.locale → the organization's default locale → English.
So a Spanish template generates Spanish prose for an English-speaking operator, which is almost
always what you want for a legal document.
The authoring format: a catalogue of examples
Everything above describes the schema. This is what it looks like on disk.
A template serializes to one .html file: YAML frontmatter carrying the metadata (including
locale, category, wordFormatId and uiDefaults) and the form schema, then the HTML body
carrying the sections — each with its own <instructions>, its <when> rules, an optional
<default> and its <case> reference examples. There is no separate rules block: a rule is a child
of the thing it describes.
The format is strict. Element order is fixed (a section holds <instructions>?, <when>*,
<default>? and then one or more <case>; a case holds <description>?, <instructions>?,
<when>? and then content), every <section> and <case> needs an id, attributes are
allow-listed, and an unknown frontmatter key is an error. Validation runs on the form the editor
actually stores, so markup the editor would silently reshape is reported instead of saved.
Each snippet below is a literal extract from a template that parses and validates — a test asserts both, so an example here cannot document syntax the validator would reject.
57 examples. Every snippet is cut verbatim from a template that parses and validates, so it is safe to copy. Agents get the same catalogue over MCP from get_template_examples.
Frontmatter
The YAML head of the file. Only these keys survive an export → import cycle.
Template metadata
frontmatter-metadatafrontmatterTitle, description, tags and the four instruction fields.
title: Contrato de arrendamiento
description: Plantilla mínima que muestra la forma del fichero y los campos escalares.
tags:
- Ejemplo
- Arrendamiento
validateDocumentInstructions: Comprueba que la renta mensual y la fianza son coherentes entre sí.
generateSectionInstructions: Usa un registro formal. Nunca inventes datos que no estén en el formulario.
titleGenerationInstructions: Usa el nombre del arrendatario y la fecha del contrato.
agentInstructions: |-
Pregunta primero por el inmueble y después por cada parte. Si el arrendatario
es una sociedad, pide también el CIF.
form:- Besides these, `aiProfiles`, `locale`, `category`, `wordFormatId` and `uiDefaults` round-trip through export/import. Any other frontmatter key is an ERROR on import.
- The three `*Instructions` fields are appended to the server's own AI prompts. `agentInstructions` is different: it is handed to the external agent interviewing the user and never enters a prompt.
agentInstructions
frontmatter-agent-instructionsfrontmatterGuidance for the MCP agent gathering data from the user.
agentInstructions: |-
Pregunta primero por el inmueble y después por cada parte. Si el arrendatario
es una sociedad, pide también el CIF.
form:- Returned by `get_template_generation_context({ templateId })`, so an agent reads it before it starts asking questions.
- Write it as instructions to a colleague taking the client call — what to ask first, what to double-check — not as instructions to a writing model.
aiProfiles
frontmatter-ai-profilesfrontmatterPick the AI profile per operation for the whole template.
aiProfiles:
generate_section: balanced
analyze_document: precision- Keys are operations (`generate_section`, `analyze_file`, `analyze_form`, `analyze_document`, `generate_title`, …); values are AI profile ids of the organization. An unknown operation or profile is an error.
- The built-in default for every operation is `balanced`. A section `model="…"` overrides `generate_section` for that section.
Form objects
One object per entity. If it repeats, it is an object.
Singleton object
object-singletonfrontmatterDocument-wide data: one instance, always.
- id: obj_contrato
label: DATOS DEL CONTRATO
objectKey: contrato
description: Datos que valen para todo el documento.
minimumCount: 1
maximumCount: 1- `maximumCount: 1` is what makes it a singleton. Use one for data that applies to the whole document.
Repeatable object
object-repeatablefrontmatterAn entity the user can add any number of: parties, properties, line items.
- id: obj_parte
label: PARTES
objectKey: parte
description: Cada interviniente en el contrato.
minimumCount: 1
maximumCount: null- `maximumCount: null` means unlimited. Its form data is keyed `parte.0.nombre`, `parte.1.nombre`, … with a `parte._count`.
- Any condition on a field of a repeatable object (`maximumCount` absent, null or > 1) must say which entries it reads: `entries="any"`, `"all"` or `"first"`.
- If it repeats, it is an object — never a field holding a list.
Object validation rule
object-validation-rulefrontmatterA prose rule checked by the AI across every entry of an object.
validationRules:
- id: val_vendedor_nif
rule: Dos vendedores no pueden compartir el mismo NIF.- Written inline on the object. The parser injects `targetObjectId` from its position — never write that key yourself.
- Evaluated by AI, not by a regex, which is what lets it compare entries to each other.
Form fields
The eight input types and their per-type properties.
string field
field-stringfrontmatterFree text, single line or multiline.
- id: field_observaciones
label: Observaciones
type: string
fieldKey: observaciones
objectId: obj_parte
required: false
description: Notas libres sobre esta parte.
multiline: true
rows: 4- `multiline: true` renders a textarea; `rows` sizes it. Also takes `defaultValue`, `sampleValue`, `placeholder`, `maxLength`.
number field
field-numberfrontmatterA numeric value with optional bounds and a unit.
- id: field_renta
label: Renta mensual
type: number
fieldKey: renta
objectId: obj_contrato
required: true
description: Importe en euros.
min: 0
max: 100000
units: €- `units` is display only — the stored value is the bare number.
date field
field-datefrontmatterAn ISO date, optionally defaulting to today.
- id: field_fecha
label: Fecha del contrato
type: date
fieldKey: fecha
objectId: obj_contrato
required: true
description: Fecha de firma.
defaultValue: today- `defaultValue: today` resolves at document creation. Also takes `minDate` / `maxDate`.
selector field
field-selectorfrontmatterA closed set of allowed values.
- id: field_duracion
label: Duración
type: selector
fieldKey: duracion
objectId: obj_contrato
required: true
description: Plazo pactado.
options:
- Un año
- Tres años
- Cinco años- A condition testing this field must use one of the declared options — compared trimmed and case-insensitively. Anything else is a validation error.
- Add `multiple: true` for a multi-select; the value is then an array.
checkbox field
field-checkboxfrontmatterA boolean, with an optional default.
- id: field_prorroga
label: Prórroga automática
type: checkbox
fieldKey: prorroga
objectId: obj_contrato
required: false
description: Marque si el contrato se prorroga sin preaviso.
defaultValue: true- In a condition the value is exactly `"true"` / `"false"` (body) or `true` / `false` (YAML), e.g. `op="is" value="false"`. Anything else — `yes`, `1` — is a validation error.
file_attachment field
field-file-attachmentfrontmatterAn uploaded file the AI extracts structured data from.
- id: field_dni
label: DNI del poderdante
type: file_attachment
fieldKey: dni
objectId: obj_poderdante
required: true
description: Adjunte anverso y reverso.
kind: DNI
multiple: true
acceptedTypes: application/pdf,image/*
analysisInstructions: |-
Es un DNI español. Extrae número, nombre, apellidos, fecha de nacimiento
y domicilio.
needAcceptance: true
schema:
type: object
properties:
numero:
type: string
nombre:
type: string
fecha_nacimiento:
type: string
format: date
required:
- numero
- nombre- `analysisInstructions` tells the analyzer what to pull out; `schema` (JSON Schema) constrains the result and is validated on save.
- `needAcceptance` defaults to `false`. Set `true` only when the template author decides a human must confirm the extraction before it counts as filled — ask them, do not assume.
- Never set a `file_attachment` value yourself — upload with `create_attachment_upload_url`, then `analyze_attachments`.
- `allowedTools` is obsolete and ignored: attachment analysis calls no tools. To derive a value from an address, put `geocode_search` in the consuming section's `tools`.
geoaddress field
field-geoaddressfrontmatterAn address, stored as a structured object with coordinates.
- id: field_domicilio
label: Domicilio
type: geoaddress
fieldKey: domicilio
objectId: obj_poderdante
required: true
description: Dirección completa; se geocodifica al guardarla.
placeholder: Calle, número, población- Pass the object returned by `geocode_address`, not a plain string.
connector_tool field
field-connector-toolfrontmatterA value fetched from one of the organization's connectors.
- id: field_titularidad
label: Titularidad registral
type: connector_tool
fieldKey: titularidad
objectId: obj_inmueble
required: false
description: Se consulta en el registro conectado a la organización.
connectorId: 7c2c1c2e-1f4a-4a1b-9f3e-0c9a1d2b3c4d
toolName: buscar_finca
multiple: false- `connectorId` and `toolName` are set once by the author. When the caller supplies the live connector list, the validator checks the reference still resolves.
- Distinct from a section `tools` entry: this fetches a FORM value, that lets a section call a tool while writing.
Field and object rules
Inline visibility and validation, written on the field or object they govern.
show_if visibility rule
field-visibility-show-iffrontmatterShow a field only when a condition holds.
visibilityRules:
- id: vr_iban
mode: show_if
effect: hide
logicOperator: and
conditions:
- fieldId: field_forma_pago
operator: equals
value: Transferencia- Inline on the field. The parser injects `targetFieldId` from its position — do not write it.
- A condition reading a field of ANOTHER, repeatable object needs `entries: any | all | first`; a sibling field of the same object reads the same entry and takes no `entries`.
- A field or object has at most one visibility rule — several rules on one target are an error.
- `required` is only enforced while the field is visible.
hide_if rule with effect: disable
field-visibility-hide-iffrontmatterGrey a field out rather than removing it, when any of several conditions holds.
visibilityRules:
- id: vr_vencimiento
mode: hide_if
effect: disable
logicOperator: or
conditions:
- fieldId: field_forma_pago
operator: not_equals
value: Aplazado
- fieldId: field_precio
operator: is_empty- `effect: disable` keeps the field visible but uneditable — prefer it when the reader benefits from knowing the field exists.
- `logicOperator: or` means any condition suffices; `and` requires all. There is no nesting.
always rule
field-visibility-alwaysfrontmatterA rule that never restricts: the field is always shown. The editor writes it when a gate is switched off.
visibilityRules:
- id: vr_nota_interna
mode: always
effect: hide
logicOperator: and
conditions: []- With `mode: always` the conditions are not evaluated and the field is ALWAYS shown — but the `conditions` key and `logicOperator` must still be present. An `always` rule with them missing is a validation error.
Field validation rule
field-validation-rulefrontmatterA prose constraint on one field, checked by AI.
validationRules:
- id: val_iban
rule: Debe ser un IBAN español válido de 24 caracteres.Form-level validation rule
form-validation-rulefrontmatterA rule about the form as a whole, targeting no single field.
validationRules:
- id: val_form_global
rule: El precio total debe ser coherente con el número de fincas transmitidas.- Sits on `form`, not on a field or object — that is what makes it form-level.
- The validator emits an informational warning ("no target (form-level rule)") for these. That is expected, not a defect in your template.
Sections
The unit of generation. `uses` is the load-bearing attribute.
uses
section-usesbodyDeclares which form objects a section's generation can see.
<section id="partes" name="PARTES" uses="obj_parte,obj_contrato">
<case id="partes_general">
<description>
Cuando interviene una persona física.
</description>
<p><strong>{nombre | ANA TORRES GIL}</strong>, mayor de edad, con domicilio en Barcelona.</p>
</case>
</section>- Every section must declare `uses` — a section without it is a validation error. Forgetting it used to mean the AI silently received no data.
- Comma-separated object ids, no spaces, no duplicates — or `uses="*"` for every object (see section-uses-all).
<default>
section-defaultbodyThe text a section starts with, before anything is generated.
<section id="diligencia" name="DILIGENCIA DE CIERRE" uses="obj_acta" editable="false">
<default>
<p>Y yo, el Notario, doy fe de cuanto antecede.</p>
</default>
<case id="diligencia_general">
<description>
Fórmula de cierre.
</description>
<p>Y yo, el Notario, doy fe de cuanto antecede y de haber identificado a los comparecientes.</p>
</case>
</section>- At most one per section. Order inside a `<section>`: `<instructions>?` then the section `<when>`s, then `<default>?`, then the `<case>`s. Anything out of that order is an error.
- It holds content only — no `<when>`, and no `{label | sample}` placeholders: it is emitted verbatim.
- This section also shows `editable="false"`, which locks the generated text in the editor.
- This is the ONLY thing that puts text in a section on document creation. `<case>` content never does — a case is reference material.
- It takes no attributes: there is one per section and nothing can reference it.
Section visibility
section-when-hidebodyDrop a whole section when the form says it does not apply.
<section id="clausula_no_residente" name="CLÁUSULA DE NO RESIDENTE" uses="obj_vendedor">
<when result="hide">
<condition field="field_no_residente" op="is" value="false" entries="all"></condition>
</when>- `result` is REQUIRED: `hide` | `disable` | `optional` | `mandatory`. A section may carry several `<when>`s; precedence is fixed, whatever their order: hide > disable > mandatory > optional. With no match the section's `visibility` applies.
- This condition reads a repeatable object, so it says `entries="all"`: the clause is hidden only when EVERY seller is resident.
- `result="static"` is NOT legal here — that belongs on a `<case>`. The validator rejects it.
- Never write "skip this section if…" in `<instructions>`: the model does not decide visibility, this does.
optional and mandatory results
section-when-optionalbodyMake a section skippable, or required, depending on the form.
<section id="advertencia_aplazado" name="ADVERTENCIA DE APLAZAMIENTO" uses="obj_operacion">
<when result="optional">
<condition field="field_forma_pago" op="is-not" value="Aplazado"></condition>
</when>
<when result="mandatory">
<condition field="field_vencimiento" op="not-empty"></condition>
</when>
<case- `optional` may be generated and is skipped without complaint; `mandatory` must be generated. Set `visibility` on the section to change what happens when NO rule matches.
disable result, and the empty operator
section-when-disablebodyKeep a section in the document but do not generate it.
<section id="nota_registral" name="NOTA REGISTRAL" uses="obj_finca,obj_operacion">
<when result="disable">
<condition field="field_referencia_catastral" op="empty" entries="any"></condition>
</when>- `disable` leaves the section present but ungenerated — use it when the reader should see that the section exists and does not apply.
- `entries="any"`: the section is disabled as soon as one property lacks its cadastral reference.
- `empty` / `not-empty` take no `value`.
Hiding a section on entry count
section-count-visibilitybodyDrop a section that only makes sense with more than one entry.
<section id="pluralidad_fincas" name="PLURALIDAD DE FINCAS" uses="obj_finca">
<instructions>
Enumera las fincas por su referencia catastral.
</instructions>
<when result="hide">
<condition object="obj_finca" op="lte" value="1"></condition>
</when>- Count operators need `object=`, never `field=`. A count threshold is a non-negative integer, written as an attribute string; anything else is an error.
- The count is derived from the entries present (`objectKey.N.*`) and `_count`, clamped to minimumCount/maximumCount.
visibility, temperature, editable
section-attributesbodyPer-section generation settings.
<section id="valoracion" name="VALORACIÓN" uses="obj_acta" visibility="optional" temperature="0.2" tools="connector:7c2c1c2e-1f4a-4a1b-9f3e-0c9a1d2b3c4d:precio_referencia">- `visibility` is the fallback when no `<when>` matches — `mandatory` (the default), `optional`, `disable` or `hide`.
- `editable="false"` locks the generated text in the editor. `temperature` is a number from 0 to 2; `model` names an AI profile (see section-model).
- Attributes are a closed list: id, name, visibility, editable, model, temperature, tools, uses, wave, sees-previous. Any other — or a typo like `visibilty` — is an error.
model
section-modelbodyUse a different AI profile for one section.
<section id="regimen" name="RÉGIMEN ECONÓMICO" model="precision" uses="obj_prestatario">- The value is an AI profile id (`fast`, `balanced`, `precision`, or one the organization defined). Unknown profiles are an error when the organization is known.
- Omit it to use the template `aiProfiles.generate_section`, then the organization default (`balanced`).
uses="*"
section-uses-allbodyLet a section read every form object, explicitly.
<section id="comparecencia" name="COMPARECENCIA" uses="*">- `*` must be the whole attribute. Prefer a list of ids when the section only needs some objects — less data in the prompt, fewer stale regenerations.
- Hidden and disabled objects never reach the AI, whatever `uses` says.
wave
section-wavebodyOrder generation: a wave starts when every lower wave has finished.
<section id="importe" name="IMPORTE" uses="obj_prestamo" wave="1">- A non-negative integer; omitted means 0. Sections sharing a wave generate concurrently.
- Use it only when a section must read what another one wrote (see section-sees-previous).
sees-previous
section-sees-previousbodyGive a section the text already generated by earlier waves.
<section id="resumen" name="RESUMEN" uses="obj_prestamo" wave="2" sees-previous="true">
<instructions>
Resume en una frase lo ya redactado en las secciones anteriores, citando el importe exactamente igual.
</instructions>
<case id="resumen_general">
<description>
Siempre.
</description>
<p>En resumen, se formaliza un préstamo de {importe | CIENTO VEINTE MIL} euros en los términos expuestos.</p>
</case>
</section>- `true` or `false`. Only meaningful with `wave` > 0 — in wave 0 nothing has been generated yet.
- Expensive and cache-hostile: the earlier text goes into the prompt. Reserve it for sections that must agree verbatim with what came before.
Cases
Reference variants the AI picks between, or emits verbatim.
<description>
case-descriptionbodyProse that tells the AI when to pick this variant.
<case id="partes_general">
<description>
Cuando interviene una persona física.
</description>
<p><strong>{nombre | ANA TORRES GIL}</strong>, mayor de edad, con domicilio en Barcelona.</p>
</case>- A case with only a `<description>` is AI-selected and AI-rewritten — the content is reference material, not literal output.
- `<description>` and `<instructions>` are ELEMENTS, not attributes, and hold no formatting marks.
- A `<case>` takes exactly one attribute, `id`, and it is required — unique across the whole template.
- Order inside a case: `<description>?` `<instructions>?` `<when>?` then content.
<when result="static">
case-staticbodyEmit this case verbatim, with no AI rewrite, when the condition holds.
<case id="precio_transferencia">
<description>
Pago mediante transferencia bancaria.
</description>
<when result="static">
<condition field="field_forma_pago" op="is" value="Transferencia"></condition>
</when>
<p>El precio se abona mediante transferencia bancaria a la cuenta designada por la parte vendedora.</p>
</case>- The presence of the `<when>` is the only thing that makes a case static — there is no separate flag.
- Write a `<description>` too, even on a static case. Staticness is decided per DOCUMENT: if no static rule in the section matches, the whole section falls through to AI generation and every case is offered to the model by its description.
- A case holds at most one `<when>`, and inside a case `result="static"` is the only legal result.
- Static content is emitted verbatim: no `{label | sample}` placeholders in it.
match="all"
case-static-match-allbodyRequire every condition instead of any one.
<when result="static" match="all">
<condition field="field_forma_pago" op="is" value="Cheque bancario"></condition>
<condition field="field_precio" op="gt" value="1000"></condition>
</when>- `match` is REQUIRED as soon as a `<when>` has more than one condition: `match="all"` (every condition) or `match="any"` (at least one). There is no default.
- With a single condition, omit it.
Conditions
The test grammar shared by every rule in the body.
Field condition
condition-isbodyThe common case: a field equals a value.
<condition field="field_forma_pago" op="is" value="Transferencia">- `op` is REQUIRED: `is`, `is-not`, `contains`, `not-contains`, `gt`, `lt`, `empty`, `not-empty`. An unknown op is an error, never silently dropped.
- `field` names the field ID, not its label or key.
- Values are typed by the field: a selector value must be one of its options, a checkbox value `true`/`false`, a number a number, a date `YYYY-MM-DD` or `today`. Text compares trimmed and case-insensitively.
Object count condition
condition-object-countbodyTest how many entries a repeating object has.
<condition object="obj_vendedor" op="gte" value="3">- Use `object=` rather than `field=`, and the count operators `gte`, `lte`, `eq`.
- The count operators work ONLY with `object=`; `field="x" op="gte"` is a validation error. A condition takes `field=` or `object=`, never both.
Condition on an analysis result
condition-pathbodyBranch on something the AI extracted from an uploaded file.
<condition field="field_pasaporte" path="nacionalidad" op="is" value="Española">- `path` is a dot path into the `file_attachment` field's analysis result, so document structure can follow the contents of an attachment.
- Only valid on a `file_attachment` field — `path` on any other field is an error.
The remaining operators
condition-operators-referencebodyOne case per operator that has no other example here.
<section id="vendedor_societario" name="VENDEDOR SOCIETARIO" uses="obj_vendedor">
<instructions>
Indica si el primer vendedor interviene como sociedad o en su propio nombre.
</instructions>
<case id="op_contains">
<description>
El nombre del primer vendedor contiene una forma societaria.
</description>
<when result="static">
<condition field="field_nombre_vendedor" op="contains" value="S.L." entries="first"></condition>
</when>
<p>La parte vendedora es una sociedad mercantil debidamente constituida.</p>
</case>
<case id="op_not_contains">
<description>
El nombre del primer vendedor no contiene forma societaria.
</description>
<when result="static">
<condition field="field_nombre_vendedor" op="not-contains" value="S.L." entries="first"></condition>
</when>
<p>La parte vendedora interviene en su propio nombre y derecho.</p>
</case>
</section>
<section id="importe_reducido" name="IMPORTE REDUCIDO" uses="obj_operacion">
<case id="op_lt">
<description>
Operaciones de importe reducido.
</description>
<when result="static">
<condition field="field_precio" op="lt" value="60000"></condition>
</when>
<p>La operación no supera el umbral de comunicación reforzada.</p>
</case>
<case id="importe_general">
<description>
Cualquier otro importe.
</description>
<p>La operación supera el umbral de comunicación reforzada por importe de {precio | CIENTO VEINTE MIL} euros.</p>
</case>
</section>
<section id="numero_vendedores" name="NÚMERO DE VENDEDORES" uses="obj_vendedor">
<case id="op_count_gte">
<description>
Tres o más vendedores.
</description>
<when result="static">
<condition object="obj_vendedor" op="gte" value="3"></condition>
</when>
<p>Los vendedores comparecen conjuntamente y por partes iguales.</p>
</case>
<case id="op_count_eq">
<description>
Exactamente un vendedor.
</description>
<when result="static">
<condition object="obj_vendedor" op="eq" value="1"></condition>
</when>
<p>La parte vendedora está formada por un único titular.</p>
</case>
<case id="vendedores_general">
<description>
Dos vendedores.
</description>
<p>Los vendedores {nombre | ANA TORRES GIL} y {nombre | LUIS PÉREZ RUIZ} comparecen en la forma que resulta del formulario.</p>
</case>
</section>- Substring operators (`contains`, `not-contains`) test text; `gt` / `lt` compare numbers and dates only; `gte` / `lte` / `eq` count object entries.
- Each section keeps its static cases MUTUALLY EXCLUSIVE — two static cases that can match the same form data are a validation error, and at generation time an error rather than "first wins".
- Text comparisons are trimmed and case-insensitive (`S.L.` matches `s.l.`). Nothing else is normalized: trailing punctuation counts.
entries="any|all|first"
condition-entriesbodySay which entries of a repeatable object a condition reads.
<section id="no_residentes" name="NO RESIDENTES" visibility="optional" uses="obj_prestatario">
<when result="mandatory" match="any">
<condition field="field_residente" op="is" value="false" entries="any"></condition>
<condition field="field_importe" op="gt" value="1000000"></condition>
</when>- `any`: true when at least one entry matches. `all`: true when every entry matches (false when there are none). `first`: only entry 0.
- REQUIRED on any condition whose field belongs to an object that can have more than one entry; not allowed on a singleton's field or on `object=` count conditions.
- Here the section becomes mandatory as soon as one borrower is non-resident — or the amount is large; `match="any"` because the `<when>` has two conditions.
entries="all" and entries="first"
condition-entries-allbodyHide a section only when every entry agrees; read just the first entry.
<section id="regimen" name="RÉGIMEN ECONÓMICO" model="precision" uses="obj_prestatario">
<when result="hide">
<condition field="field_estado_civil" op="is" value="Soltero" entries="all"></condition>
</when>- Hidden when EVERY borrower is single — one married borrower keeps the section.
- See the `representacion` section of the same fixture for `entries="first"`.
gt / lt on number and date
condition-typed-gt-ltbodyCompare a number or a date; every condition must hold with match="all".
<when result="static" match="all">
<condition field="field_importe" op="gt" value="300000"></condition>
<condition field="field_fecha_firma" op="lt" value="2027-01-01"></condition>
</when>- `gt` / `lt` exist only for `number` and `date` fields; on any other type they are an error.
- A number value must be a number (`300000`, no thousands separators); a date value an ISO `YYYY-MM-DD` or `today`.
Content
What may appear inside a section, and placeholders.
Static text outside a section
content-static-textbodyText that always appears verbatim, with no AI involvement.
<p><strong>CONTRATO DE ARRENDAMIENTO</strong></p>- Plain HTML outside any `<section>`. Do NOT wrap always-present, unconditional text in a section.
- A section exists to let the AI choose between variants, or to be governed by visibility rules. If neither applies, it should not be a section.
{label | sample} placeholder
content-placeholder-authoringbodyMark a dynamic value inside reference content.
<p>En Barcelona, a {fecha | tres de julio de dos mil veinticinco}.</p>- The part after `|` is a realistic sample so the model can see the expected shape. Never let a sample reach the finished document.
- Only in reference content the AI rewrites (a case without `<when>`). In a static case, a `<default>` or text outside sections it is a validation error — that content is emitted verbatim.
- When the value is missing at generation time the model emits a placeholder chip instead.
Placeholder chip
content-placeholder-chipbodyThe node the generator emits for a value it does not have.
<span data-type="placeholder" data-name="BASE IMPONIBLE" data-description="Importe sobre el que se liquida el impuesto" data-field="escritura.base_imponible">BASE IMPONIBLE</span>- `data-field` carries the canonical `objectKey.fieldKey`, and it must name a field of the form — anything else is an error. Filling the chip then writes that form field. Omit it and the value goes to document memory instead.
- `data-description` explains what is missing — it is what the person filling the gap reads.
Headings, lists, tables, page breaks
content-rich-blocksbodyThe block content a section may hold beyond paragraphs.
<h2>Exposición</h2>
<p>Comparece <span data-bookmark="OTORGANTE"><strong>{otorgante | ANA TORRES GIL}</strong></span>, cuyos datos constan al inicio.</p>
<ul>
<li>Primera manifestación.</li>
<li>Segunda manifestación.</li>
</ul>
<ol>
<li>Primer acuerdo.</li>
<li>Segundo acuerdo.</li>
</ol>- Allowed inside a section: paragraph, heading (h1–h4), bullet list, ordered list, blockquote, horizontal rule, table, image, page break.
- Bold is `<strong>`. Separate paragraphs with `<p>`.
Page break
content-page-breakbodyForce the exported DOCX onto a new page.
<div data-page-break="true">
</div>- Never fake one with empty paragraphs — they reflow when the content above changes length.
Bookmarks and field references
Never hardcode a page, folio or cross-reference — these resolve on export.
Automatic clause numbering
clause-numberingbodyIndependent series, semantic ordinals and cross-references. Placement follows effective document order.
<numbering><series id="otorgamientos" style="ordinal" start="1" suffix=".-"></series><series id="advertencias" style="decimal" start="1" suffix=".-"></series></numbering>- Use ord with series and key at the start of each clause, and ref with the same attributes inside remitting sentences.
- Use the preparation sheet to identify the clause by meaning, not its number in the model.
- The numbering fixture is fictional and covers three optional clauses.
Clause reference
clause-referencebodyA remitting sentence resolves its target number after document composition.
<p>Remisión a su otorgamiento <ref series="otorgamientos" key="no_residente"></ref>.</p>Bookmark
reference-bookmarkbodyAnchor a fragment so other parts of the document can point at it.
<span data-bookmark="OTORGANTE"><strong>{otorgante | ANA TORRES GIL}</strong></span>- The name is yours to choose, within Word's rules: start with a letter; letters, digits and `_` only; no spaces; at most 40 characters. Names are unique in a template.
- It is what every `data-bookmark-name` refers to; a reference to a bookmark no `data-bookmark` declares is an error.
REF — repeat bookmarked text
reference-refbodyInsert the text of a bookmark instead of retyping it.
<span data-field-ref="true" data-field-type="REF" data-bookmark-name="OTORGANTE"></span>- Resolved on DOCX export. Retyping the text by hand is how two copies drift.
PAGEREF and FOLIOREF
reference-pagerefbodyThe page or folio a bookmark falls on.
<p>Consta en la página <span data-field-ref="true" data-field-type="PAGEREF" data-bookmark-name="PROTOCOLO"></span> y en el folio <span data-field-ref="true" data-field-type="FOLIOREF" data-bookmark-name="PROTOCOLO"></span>.</p>NUMPAGES, NUMFOLIOS and their _TEXT forms
reference-countsbodyTotals for the whole document. These need no bookmark.
<p>La presente escritura consta de <span data-field-ref="true" data-field-type="NUMPAGES"></span> páginas (<span data-field-ref="true" data-field-type="NUMPAGES_TEXT"></span>) y <span data-field-ref="true" data-field-type="NUMFOLIOS"></span> folios (<span data-field-ref="true" data-field-type="NUMFOLIOS_TEXT"></span>).</p>- A `*_TEXT` variant renders the number in words — "tres" rather than "3".
- A phrase like "extendida en tres folios" must use `NUMFOLIOS_TEXT`, never literal text.
PREV_PAGES and PREV_FOLIOS
reference-prevbodyHow many pages or folios precede this point. No bookmark needed.
<p>Extendida en las <span data-field-ref="true" data-field-type="PREV_PAGES"></span> páginas anteriores (<span data-field-ref="true" data-field-type="PREV_PAGES_TEXT"></span>) y en los <span data-field-ref="true" data-field-type="PREV_FOLIOS"></span> folios anteriores (<span data-field-ref="true" data-field-type="PREV_FOLIOS_TEXT"></span>).</p>- A phrase like "los cuatro folios anteriores" must use `PREV_FOLIOS_TEXT`.
REMAINING_PAGES
reference-remainingbodyHow much of the document follows a bookmark.
<p>Restan <span data-field-ref="true" data-field-type="REMAINING_PAGES" data-bookmark-name="PROTOCOLO"></span> páginas (<span data-field-ref="true" data-field-type="REMAINING_PAGES_TEXT" data-bookmark-name="PROTOCOLO"></span>).</p>PAGES_BEFORE and FOLIOS_BEFORE
reference-beforebodyHow much of the document precedes a bookmark.
<p>Antes del marcador hay <span data-field-ref="true" data-field-type="PAGES_BEFORE" data-bookmark-name="OTORGANTE"></span> páginas (<span data-field-ref="true" data-field-type="PAGES_BEFORE_TEXT" data-bookmark-name="OTORGANTE"></span>) y <span data-field-ref="true" data-field-type="FOLIOS_BEFORE" data-bookmark-name="OTORGANTE"></span> folios (<span data-field-ref="true" data-field-type="FOLIOS_BEFORE_TEXT" data-bookmark-name="OTORGANTE"></span>).</p>- Measured to a bookmark, unlike `PREV_*`, which measures to the current position.
PAGES_BETWEEN and FOLIOS_BETWEEN
reference-betweenbodyCounts measured between two bookmarks.
<p>Entre ambos marcadores median <span data-field-ref="true" data-field-type="PAGES_BETWEEN" data-bookmark-name="PROTOCOLO" data-bookmark-name-end="OTORGANTE"></span> páginas (<span data-field-ref="true" data-field-type="PAGES_BETWEEN_TEXT" data-bookmark-name="PROTOCOLO" data-bookmark-name-end="OTORGANTE"></span>) y <span data-field-ref="true" data-field-type="FOLIOS_BETWEEN" data-bookmark-name="PROTOCOLO" data-bookmark-name-end="OTORGANTE"></span> folios (<span data-field-ref="true" data-field-type="FOLIOS_BETWEEN_TEXT" data-bookmark-name="PROTOCOLO" data-bookmark-name-end="OTORGANTE"></span>).</p>- The `*_BETWEEN` types are the only ones that take — and require — a second bookmark, `data-bookmark-name-end`.
Section tools
Extra AI tools a section may call while generating.
tools
section-toolsbodyLet a section call an extra AI tool while it generates.
<section id="localizacion" name="LOCALIZACIÓN" uses="obj_acta" tools="geocode_search">- `math` and `update_memory` are always available and must NOT be listed. `geocode_search` is opt-in, which is what this attribute is for.
- An unknown token is a validation error. Call `list_section_tools` to see what this organization can use — connector tokens differ per organization.
- A section reads the document's shared memory but does not write the form: values that map to a field are emitted as placeholders instead.
Connector tool token
section-tools-connectorfrontmatterGive a section access to one tool on an organization connector.
tools="connector:7c2c1c2e-1f4a-4a1b-9f3e-0c9a1d2b3c4d:precio_referencia"- The token is `connector:<connectorId>:<toolName>`. Get real ones from `list_section_tools`.
- A token whose connector has been deleted or disabled is an error whenever the validator can see the organization's connector list (validate_template, import, publish).