Concepts

    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.

    Rendering diagram…
    One schema, three consumers

    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:

    formData
    {
      "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

    TypeValue shape
    stringText.
    numberNumeric.
    dateISO date string.
    checkboxBoolean.
    selectorOne of a declared set of allowed values.
    file_attachmentAn object wrapper — an uploaded file, optionally with an AI analysis of it.
    geoaddressAn object wrapper — a structured address with coordinates.
    connector_toolAn 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-metadatafrontmatter

    Title, 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-instructionsfrontmatter

    Guidance 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-profilesfrontmatter

    Pick 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-singletonfrontmatter

    Document-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-repeatablefrontmatter

    An 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-rulefrontmatter

    A 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-stringfrontmatter

    Free 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-numberfrontmatter

    A 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-datefrontmatter

    An 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-selectorfrontmatter

    A 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-checkboxfrontmatter

    A 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-attachmentfrontmatter

    An 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-geoaddressfrontmatter

    An 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-toolfrontmatter

    A 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-iffrontmatter

    Show 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-iffrontmatter

    Grey 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-alwaysfrontmatter

    A 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-rulefrontmatter

    A 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-rulefrontmatter

    A 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-usesbody

    Declares 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-defaultbody

    The 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-hidebody

    Drop 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-optionalbody

    Make 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-disablebody

    Keep 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-visibilitybody

    Drop 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-attributesbody

    Per-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-modelbody

    Use 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-allbody

    Let 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-wavebody

    Order 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-previousbody

    Give 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-descriptionbody

    Prose 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-staticbody

    Emit 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-allbody

    Require 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-isbody

    The 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-countbody

    Test 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-pathbody

    Branch 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-referencebody

    One 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-entriesbody

    Say 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-allbody

    Hide 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-ltbody

    Compare 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-textbody

    Text 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-authoringbody

    Mark 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-chipbody

    The 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-blocksbody

    The 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-breakbody

    Force 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-numberingbody

    Independent 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-referencebody

    A 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-bookmarkbody

    Anchor 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-refbody

    Insert 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-pagerefbody

    The 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-countsbody

    Totals 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-prevbody

    How 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-remainingbody

    How 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-beforebody

    How 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-betweenbody

    Counts 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-toolsbody

    Let 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-connectorfrontmatter

    Give 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).