# Text layers

Create dynamic documents with Paradoc templates

Canonical URL: https://docs.paradoc.dev/guides/text-layers/

In this guide, we'll use Paradoc to create a residential lease agreement. The lease is a perfect example because it combines field values (rent amount, dates), party information (landlord and tenants), conditionals (smoking allowed?), and loops (multiple tenants) — all common patterns in document generation.

**About Text Layers**

Text layers use Paradoc templates to generate documents. You write placeholders
like `{{fields.monthlyRent}}`, and Paradoc fills them with your data. Everything
inside `{{ }}` is an [artifact expression](/concepts/logic), the same language
as field conditions and computed values, so Markdown, HTML, plain text, and
DOCX templates follow the same rules as the rest of the artifact.

## Set Up Your Project

Create a new project and set up the directory structure:

**npm**

```bash
mkdir my-lease-project && cd my-lease-project
mkdir -p layers src output
npm init -y
```

**pnpm**

```bash
mkdir my-lease-project && cd my-lease-project
mkdir -p layers src output
pnpm init
```

**yarn**

```bash
mkdir my-lease-project && cd my-lease-project
mkdir -p layers src output
yarn init -y
```

**bun**

```bash
mkdir my-lease-project && cd my-lease-project
mkdir -p layers src output
bun init -y
```

Your project structure should look like this:

```
my-lease-project/
├── layers/
│   └── lease.md          # Your Paradoc template
├── src/                  # Your code goes here
├── output/               # Generated documents
└── package.json
```

## Install

Install the SDK, unified renderer, and filesystem resolver.

**npm**

```bash
npm install @paradoc/sdk @paradoc/render @paradoc/resolvers
```

**pnpm**

```bash
pnpm add @paradoc/sdk @paradoc/render @paradoc/resolvers
```

**yarn**

```bash
yarn add @paradoc/sdk @paradoc/render @paradoc/resolvers
```

**bun**

```bash
bun add @paradoc/sdk @paradoc/render @paradoc/resolvers
```

## Create the Template

Create a Paradoc template at `layers/lease.md`:

```markdown
# RESIDENTIAL LEASE AGREEMENT

This Lease Agreement is entered into on **{{fields.startDate}}** between:

**LANDLORD:** {{parties.landlord.name}}

**TENANT(S):**
{{#each parties.tenant}}
- {{item.name}}
{{/each}}

---

## 1. PROPERTY

The Landlord agrees to rent to the Tenant the property located at:

{{fields.address}}

**Property Type:** {{fields.propertyType}}
**Bedrooms:** {{fields.bedrooms}}

---

## 2. TERM

The lease term shall be **{{fields.leaseTerm}}**, starting on **{{fields.startDate}}**.

---

## 3. RENT

The Tenant agrees to pay **{{fields.monthlyRent}}** per month, due on the first of each month.

---

## 4. RULES

{{#if fields.petsAllowed}}
Pets are **permitted** with prior written approval.
{{else}}
Pets are **not permitted** on the premises.
{{/if}}

{{#if fields.smokingAllowed}}
Smoking is **permitted** in designated areas only.
{{else}}
Smoking is **not permitted** on the premises.
{{/if}}

---

## 5. SIGNATURES

**LANDLORD:**

Name: {{parties.landlord.name}}

Signature: ___________________________  Date: ____________


**TENANT(S):**

{{#each parties.tenant}}
Name: {{item.name}}

Signature: ___________________________  Date: ____________

{{/each}}
```

Key syntax patterns:

* `{{fields.fieldName}}` — Output a field value, formatted for its type
* `{{parties.role.property}}` — Access party data
* `{{annexes.slot.name}}` — An attachment's file name; `{{annexes.slot}}` prints the name and MIME type
* `{{#each parties.tenant}}...{{/each}}` — Loop over a list; `item` is the current row
* `{{#if condition}}...{{else}}...{{/if}}` — Conditional content; the condition is a boolean expression

## Define the Form

Create a form that references the template file. The `markdown` layer is
file-backed, so bind a resolver when you construct the form — every instance
you derive from `leaseAgreement` later carries the same resolver.

```ts
import { p } from '@paradoc/sdk'
import { createFsResolver } from '@paradoc/resolvers/fs'

const resolver = createFsResolver({ root: '.' })

const leaseAgreement = p.form({
  name: 'lease-agreement',
  title: 'Residential Lease Agreement',
  fields: {
    address: { type: 'address', label: 'Property Address', required: true },
    propertyType: {
      type: 'enum',
      label: 'Property Type',
      enum: [{ value: 'apartment' }, { value: 'house' }, { value: 'condo' }, { value: 'townhouse' }],
      required: true,
    },
    bedrooms: { type: 'number', label: 'Bedrooms', min: 0, max: 10, required: true },
    startDate: { type: 'date', label: 'Lease Start Date', required: true },
    leaseTerm: { type: 'duration', label: 'Lease Term', required: true },
    monthlyRent: { type: 'money', label: 'Monthly Rent', required: true },
    petsAllowed: { type: 'boolean', label: 'Pets Allowed', default: false },
    smokingAllowed: { type: 'boolean', label: 'Smoking Allowed', default: false },
  },
  parties: {
    landlord: {
      label: 'Landlord',
      partyType: 'person',
      min: 1,
      max: 1,
      signature: { required: true },
    },
    tenant: {
      label: 'Tenant',
      partyType: 'person',
      min: 1,
      max: 4,
      signature: { required: true },
    },
  },
  defaultLayer: 'markdown',
  layers: {
    markdown: {
      kind: 'file',
      mimeType: 'text/markdown',
      path: 'layers/lease.md',
    },
  },
}, { resolver })
```

## Fill the Form

Create a runtime instance by filling the form with field and party data:

```ts
const filled = leaseAgreement.fill({
  fields: {
    address: {
      line1: '123 Oak Street, Unit 4B',
      locality: 'San Francisco',
      region: 'CA',
      postalCode: '94102',
      country: 'US',
    },
    propertyType: 'apartment',
    bedrooms: 2,
    startDate: '2025-02-01',
    leaseTerm: 'P12M', // ISO 8601 duration: 12 months
    monthlyRent: { amount: 2800, currency: 'USD' },
    petsAllowed: true,
    smokingAllowed: false,
  },
  parties: {
    landlord: { id: 'landlord-0', name: 'Alice Chen' },
    tenant: [
      { id: 'tenant-0', name: 'Bob Smith' },
      { id: 'tenant-1', name: 'Carol Johnson' },
    ],
  },
})

if (!filled.isValid()) {
  console.log('Errors:', filled.validate().errors)
}
```

## Render the Document

Render the filled form using the MIME-selected renderer. The resolver bound
to `leaseAgreement` travels with `filled`, so the render call doesn't repeat
it:

```ts
import { createLayerRenderer } from '@paradoc/render'
import { writeFileSync } from 'node:fs'

const output = await filled.render({
  renderer: createLayerRenderer(),
  layer: 'markdown',
})

writeFileSync('output/lease-agreement.md', output)
```

The output is a string containing your rendered Markdown document with all placeholders filled in.

You've now created a form that generates lease agreements from a Paradoc
template. The same rendering entry point works for HTML, plain text, PDF, and
DOCX layers because selection follows the layer's `mimeType`.

## Template Syntax Reference

Everything inside a marker is an [artifact expression](/concepts/logic). Templates see the same values as field conditions: `fields`, computed values by name, `parties`, and, in a checklist, `items`.

| Pattern        | Example                                                            | Description                                                            |
| -------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| Field value    | `{{fields.monthlyRent}}`                                           | Output a field value, formatted for its type                           |
| Nested value   | `{{fields.address.line1}}`                                         | Access nested properties                                               |
| Party data     | `{{parties.landlord.name}}`                                        | Access party information                                               |
| Computed value | `{{totalDue}}`                                                     | Output a `defs` value by name                                          |
| Expression     | `{{fields.bedrooms * 2}}`, `{{sum(fields.lines.amount)}}`          | Arithmetic, functions, and aggregates                                  |
| Fallback       | `{{coalesce(fields.nickname, fields.name)}}`                       | The first value that is present                                        |
| Conditional    | `{{#if fields.petsAllowed}}...{{/if}}`                             | The condition must be boolean                                          |
| If/else        | `{{#if fields.bedrooms > 2}}...{{else}}...{{/if}}`                 | Conditional with fallback                                              |
| Unless         | `{{#unless fields.petsAllowed}}...{{/unless}}`                     | Content when the condition is false                                    |
| Loop           | `{{#each parties.tenant}}{{item.name}}{{/each}}`                   | Iterate over a list; `item` is the current row                         |
| Enclosing row  | `{{parent.name}}`                                                  | The outer row inside a nested loop                                     |
| Loop position  | `{{index(item)}}`, `{{#if first(item)}}`, `{{#unless last(item)}}` | Zero-based position, first row, last row; only inside a loop           |
| Raw HTML       | `{{{fields.notes}}}`                                               | Output without HTML escaping; only `text/html` layers escape values    |
| Signing mark   | `{{signature(parties.landlord, "landlord-sign")}}`                 | Place a signature; inside a party loop, `{{signature("tenant-sign")}}` |
| Comment        | `{{!-- comment --}}`                                               | Not rendered in output                                                 |

A condition such as `{{#if fields.notes != null}}` must be boolean: compare a text or number value rather than testing it directly.

### Replaced syntax

Templates used to have their own helper syntax. It is no longer accepted, and validation names the replacement:

| Old                                                  | Now                                                    |
| ---------------------------------------------------- | ------------------------------------------------------ |
| `{{#if (eq a "x")}}`, `ne`, `gt`, `gte`, `lt`, `lte` | `{{#if a == "x"}}`, `!=`, `>`, `>=`, `<`, `<=`         |
| `(and a b)`, `(or a b)`, `(not a)`                   | `a and b`, `a or b`, `not a`                           |
| `(contains list "x")`                                | `"x" in list` or `contains(list, "x")`                 |
| `{{default value "n/a"}}`                            | `{{coalesce(value, "n/a")}}`                           |
| `{{#with parties.landlord}}{{name}}{{/with}}`        | `{{parties.landlord.name}}`                            |
| `{{this}}`, `{{../name}}`, `{{@root.name}}`          | `{{item}}`, `{{parent.name}}`, `{{fields.name}}`       |
| `{{@index}}`, `{{@first}}`, `{{@last}}`              | `{{index(item)}}`, `{{first(item)}}`, `{{last(item)}}` |
| `{{signature "loc"}}` inside `{{#with}}`             | `{{signature(parties.role, "loc")}}`                   |
