# Form Builder

Create Form artifacts with fields, parties, and signing flows

Canonical URL: https://docs.paradoc.dev/sdk/artifacts/form/

Create Form artifacts for interactive forms with fields, signing parties, and document annexes.

## Examples

### Creating a form

```ts
// Object pattern
const form = p.form({
  name: 'lease-agreement',
  version: '1.0.0',
  title: 'Residential Lease Agreement',
  fields: {
    tenantName: { type: 'text', label: 'Tenant Name', required: true },
    moveInDate: { type: 'date', label: 'Move-in Date' },
    monthlyRent: { type: 'money', label: 'Monthly Rent', min: 0 }
  },
  parties: {
    tenant: { label: 'Tenant', partyType: 'person', signature: { required: true } },
    landlord: { label: 'Landlord', signature: { required: true } }
  },
  annexes: {
    photoId: { title: 'Photo ID', required: true }
  },
  layers: {
    html: { kind: 'inline', mimeType: 'text/html', text: '<p>Tenant: {{fields.tenantName}}</p>' }
  },
  defaultLayer: 'html'
})

// Builder pattern
const form = p.form()
  .name('lease-agreement')
  .version('1.0.0')
  .title('Residential Lease Agreement')
  .field('tenantName', field.text().label('Tenant Name').required().build())
  .field('moveInDate', field.date().label('Move-in Date').build())
  .field('monthlyRent', field.money().label('Monthly Rent').min(0).build())
  .party('tenant', party().label('Tenant').partyType('person')
    .signature({ required: true }).build())
  .party('landlord', party().label('Landlord')
    .signature({ required: true }).build())
  .annex('photoId', annex().title('Photo ID').required().build())
  .inlineLayer('html', { mimeType: 'text/html', text: '<p>Tenant: {{fields.tenantName}}</p>' })
  .defaultLayer('html')
  .build()
```

### Adding instructions

```ts
// Inline instructions
const form = p.form({
  name: 'w9-form',
  instructions: { kind: 'inline', text: 'See IRS instructions for Form W-9...' },
  agentInstructions: { kind: 'inline', text: 'Present fields in this order: name, TIN, address.' },
  // ...fields, parties, layers
})

// File-backed instructions (builder pattern)
const form = p.form()
  .name('w9-form')
  .instructions({ kind: 'file', path: './w9-instructions.md', mimeType: 'text/markdown' })
  .agentInstructions({ kind: 'inline', text: 'Present fields in this order: name, TIN, address.' })
  .build()
```

### Loading from external data

```ts
// Parse and validate unknown input (throws on error)
const form = p.form.from(jsonData)

// Safe parsing (returns result object)
const result = p.form.safeFrom(jsonData)
if (result.success) {
  const form = result.data
}
```

### Form Lifecycle

Forms follow a three-phase lifecycle: Draft → Signable → Executed.

```ts
// 1. Fill form to create DraftForm (mutable data)
const draft = form.fill({
  fields: { tenantName: 'John Doe', moveInDate: '2024-02-01' },
  annexes: { photoId: { name: 'photo-id.pdf', mimeType: 'application/pdf' } },
  parties: {
    tenant: { id: 'tenant-0', name: 'John Doe' },
    landlord: { id: 'landlord-0', name: 'ABC Property LLC', legalName: 'ABC Property LLC' }
  }
})

// 2. Configure signers and signatories
const ready = draft
  .addSigner('john', {
    person: { name: 'John Doe' },
    adopted: { signature: { image: 'data:...', method: 'drawn' } }
  })
  .addSignatory('tenant', 'tenant-0', { signerId: 'john' })
  .addSigner('abc-property', {
    person: { name: 'Bob Manager' },
    adopted: { signature: { image: 'data:...', method: 'drawn' } }
  })
  .addSignatory('landlord', 'landlord-0', { signerId: 'abc-property', capacity: 'Property manager' })

// 3. Prepare for signing (SignableForm - data frozen)
const signable = ready.prepareForSigning()

// 4. Capture signatures
const signed = signable
  .captureSignature('tenant', 'tenant-0', 'john', 'final-sig')
  .captureSignature('landlord', 'landlord-0', 'abc-property', 'final-sig')

// 5. Finalize (ExecutedForm - fully frozen)
const executed = signed.finalize()

// 6. Render the executed form
const output = await executed.render()
```

## API

### Object Pattern

```ts
p.form(input: FormInput, options?: ArtifactInstanceOptions): FormInstance
```

`options.resolver` reads the bytes of any file-backed layer this form
declares. It's bound once here, at construction, and every instance derived
from the form — every `fill`, mutator, and `clone` — carries it; `render()`
itself takes no resolver. See [ArtifactInstanceOptions](/sdk/resolvers#artifactinstanceoptions).

#### Parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` | Unique identifier; must follow slug constraints |
| `version?` | `string` | Artifact version (semantic versioning) |
| `title?` | `string` | Human-friendly name presented to end users |
| `description?` | `string` | Long-form description or context |
| `code?` | `string` | The form number as printed by its issuer (e.g. W-9, ACORD 25), or a self-issued form's own number |
| `language?` | `string` | BCP 47 source language tag; defaults to en when omitted |
| `issuer?` | `string` | Organization that issues the form |
| `edition?` | `ArtifactEdition` | Issuer edition: { key, label, date?, effectiveFrom? } |
| `metadata?` | `Metadata` | Custom metadata map |
| `instructions?` | `ContentRef` | Domain or compliance reference content |
| `agentInstructions?` | `ContentRef` | LLM/agent prompts for presentation guidance |
| `defs?` | `DefsSection` | Named typed computed values for conditional logic |
| `rules?` | `RulesSection` | Form-level validation rules |
| `fields?` | `Record<string, FormField>` | Field definitions keyed by identifier |
| `layers?` | `Record<string, Layer>` | Named layers for rendering |
| `defaultLayer?` | `string` | Key of default layer for rendering |
| `annexes?` | `Record<string, FormAnnex>` | Predefined annex slots keyed by identifier |
| `allowAdditionalAnnexes?` | `boolean` | Allow ad-hoc annexes beyond defined slots |
| `parties?` | `Record<string, FormParty>` | Party role definitions |

#### Returns

Returns a `FormInstance` with the following properties and methods:

| Name | Type | Description |
| --- | --- | --- |
| `kind` | `'form'` | Artifact discriminator |
| `name` | `string` | Form name |
| `version` | `string \| undefined` | Semantic version |
| `title` | `string \| undefined` | Human-readable title |
| `description` | `string \| undefined` | Description text |
| `code` | `string \| undefined` | The form number as printed by its issuer (e.g. W-9, ACORD 25), or a self-issued form's own number |
| `language` | `string \| undefined` | BCP 47 source language tag |
| `issuer` | `string \| undefined` | Issuing organization |
| `edition` | `ArtifactEdition \| undefined` | Issuer edition |
| `metadata` | `Metadata \| undefined` | Custom metadata map |
| `instructions` | `ContentRef \| undefined` | Domain or compliance reference content |
| `agentInstructions` | `ContentRef \| undefined` | LLM/agent prompts for presentation guidance |
| `defs` | `DefsSection \| undefined` | Named typed computed values |
| `fields` | `Record<string, FormField> \| undefined` | Field definitions |
| `layers` | `Record<string, Layer> \| undefined` | Render layers |
| `defaultLayer` | `string \| undefined` | Default layer key |
| `annexes` | `Record<string, FormAnnex> \| undefined` | Annex slot definitions |
| `allowAdditionalAnnexes` | `boolean \| undefined` | Allow ad-hoc annexes |
| `parties` | `Record<string, FormParty> \| undefined` | Party role definitions |

**Methods**

| Name | Type | Description |
| --- | --- | --- |
| `parseData` | `(data: Record<string, unknown>) => InferFormPayload` | Validate data against form (throws on error) |
| `safeParseData` | `(data: Record<string, unknown>) => ValidationResult` | Validate data (returns result object) |
| `validateFieldInput` | `(input: { fieldPath: string \| string[]; value: unknown }) => ProgressiveValidationResult<unknown>` | Validate one field input at fields.<path> |
| `validateFieldsPatch` | `(fields: unknown) => ProgressiveValidationResult<Record<string, unknown>>` | Validate a partial fields patch without requiring unrelated fields |
| `validatePartyInput` | `(input: { roleId: string; index?: number; value: unknown }) => ProgressiveValidationResult<{ roleId; index; party }>` | Validate one party input and normalize runtime party id |
| `validatePartiesPatch` | `(parties: unknown) => ProgressiveValidationResult<Record<string, RuntimeParty \| RuntimeParty[]>>` | Validate a partial parties patch |
| `validateAnnexInput` | `(input: { annexId: string; value: unknown }) => ProgressiveValidationResult<Attachment>` | Validate one annex input key/value; the value must be an Attachment |
| `validateAnnexesPatch` | `(annexes: unknown) => ProgressiveValidationResult<Record<string, Attachment>>` | Validate a partial annexes patch; each value must be an Attachment |
| `fill` | `(seed?: ProgressiveFormPayload, options?: FillOptions) => DraftForm` | Create a draft from empty, partial, or complete data; supplied values validate immediately |
| `safeFill` | `(seed?: ProgressiveFormPayload, options?: FillOptions) => SafeFillResult` | Safely create a draft; success means the supplied patch was accepted |
| `render` | `(options?: RenderOptions) => Promise<Output>` | Render a layer, with `data` if supplied. See [Render options](#render-options) |
| `clone` | `() => FormInstance` | Deep clone the instance |
| `validate` | `(options?: ValidateOptions) => StandardSchemaV1.Result` | Validate the form definition |
| `isValid` | `(options?: ValidateOptions) => boolean` | Check if form is valid |
| `toJSON` | `(options?: SerializationOptions) => object` | Serialize to JSON |
| `toYAML` | `(options?: SerializationOptions) => string` | Serialize to YAML |

### Builder Pattern

Chain methods to build a form incrementally:

Start with `p.form()`. Each method returns `FormBuilder` and can be chained.

| Method | Description |
| --- | --- |
| `from(value: Form)` | Initialize from an existing form definition, keeping every definition key |
| `name(value: string)` | Set form name (required) |
| `version(value: string)` | Set semantic version |
| `title(value: string)` | Set human-readable title |
| `description(value: string)` | Set description |
| `code(value: string)` | Set external reference code |
| `language(value: string)` | Set source language (BCP 47 tag) |
| `issuer(value: string)` | Set the issuing organization |
| `edition({ key, label, date?, effectiveFrom? })` | Set the issuer edition |
| `metadata(value: Metadata)` | Set custom metadata |
| `instructions(value: ContentRef)` | Set domain/compliance reference content |
| `agentInstructions(value: ContentRef)` | Set LLM/agent presentation guidance |
| `defs(value: DefsSection)` | Set all typed computed values |
| `def(name: string, expression: string \| Expression)` | Add a single typed computed value |
| `rules(value: RulesSection)` | Set form-level validation rules |
| `field(id: string, field: FormField)` | Add a single field |
| `fields(fields: Record<string, FormField>)` | Set all fields at once |
| `layers(value: Record<string, Layer>)` | Set all layers at once |
| `layer(key: string, layer: Layer)` | Add a layer |
| `inlineLayer(key: string, layer: { mimeType, text, ... })` | Add inline text layer |
| `fileLayer(key: string, layer: { mimeType, path, ... })` | Add file-backed layer |
| `defaultLayer(key: string)` | Set default layer for rendering |
| `annex(annexId: string, annex: FormAnnex)` | Add a single annex slot |
| `annexes(annexes: Record<string, FormAnnex>)` | Set all annexes at once |
| `allowAdditionalAnnexes(value: boolean)` | Allow ad-hoc annexes |
| `party(roleId: string, party: FormParty)` | Add a party role |
| `parties(parties: Record<string, FormParty>)` | Set all parties at once |

Terminal methods return `FormInstance`.

| Method | Description |
| --- | --- |
| `build(options?: ArtifactInstanceOptions)` | Build and validate; bind a resolver for file-backed layers |

### Static Methods

Parse forms from unknown data sources. Both accept an
[`ArtifactInstanceOptions`](/sdk/resolvers#artifactinstanceoptions) to bind a
resolver for any file-backed layer:

| Method | Returns | Description |
| --- | --- | --- |
| `from(input: unknown, options?: ArtifactInstanceOptions)` | `FormInstance` | Parse unknown input (throws on error) |
| `safeFrom(input: unknown, options?: ArtifactInstanceOptions)` | `Result<FormInstance>` | Parse unknown input (returns result object) |

## Data Validation

FormInstance provides Zod-style methods for validating data:

```ts
// Throws FormValidationError if validation fails
const validated = form.parseData({ fields: { tenantName: 'John' } })

// Returns result object
const result = form.safeParseData({ fields: { tenantName: 'John' } })
if (result.success) {
  console.log(result.data)
} else {
  console.error(result.errors)
}
```

## Progressive Validation

Use progressive validators to validate one user answer at a time before calling `fill()`.

### Return shape

All progressive validators return:

```ts
type ProgressiveValidationResult<T> =
  | { success: true; value: T; errors: null }
  | { success: false; value: null; errors: ValidationError[] }
```

### Field input validation

```ts
const species = form.validateFieldInput({
  fieldPath: 'species',
  value: 'cat',
})

const nested = form.validateFieldInput({
  fieldPath: ['profile', 'nickname'],
  value: 'Toby',
})

const partialFields = form.validateFieldsPatch({
  species: 'dog',
  weight: 20,
})
```

### Party input validation

```ts
const tenantResult = form.validatePartyInput({
  roleId: 'tenant',
  value: { name: 'John Smith' },
})

// For multi-party roles, pass an index.
const witnessResult = form.validatePartyInput({
  roleId: 'witness',
  index: 1,
  value: { name: 'Jane Witness' },
})

const partiesPatch = form.validatePartiesPatch({
  tenant: { name: 'John Smith' },
  landlord: { name: 'Acme Inc', legalName: 'Acme Inc' },
})
```

`validatePartyInput` normalizes runtime IDs using `{roleId}-{index}` (for example `tenant-0`).

### Annex input validation

```ts
const annexResult = form.validateAnnexInput({
  annexId: 'photoId',
  value: { name: 'photo-id.pdf', mimeType: 'application/pdf' },
})

const annexPatch = form.validateAnnexesPatch({
  photoId: { name: 'photo-id.pdf', mimeType: 'application/pdf' },
})
```

Every annex value must be an [Attachment](/schemas/primitives/attachment): `{ name, mimeType, checksum? }` with no other keys. A value of any other shape fails with an error under `annexes.<annexId>`. `fill`, `update`, and `setAnnex` apply the same check.

When `allowAdditionalAnnexes` is `false`, unknown annex keys are rejected. When it is `true`, an unknown key is accepted and its value must still be an Attachment. The progressive validators and full payload validation (`validateFormData`, `fill`) apply the same rule.

## Incremental Filling

Use `fill` to create a DraftForm from partial (or empty) data, then progressively fill it with `update`. This is the recommended pattern for AI agents and step-by-step UIs that collect data one field at a time.

### Creating a draft from partial data

```ts
// Start with empty data
const draft = form.fill()

// Start with some known values
const draft = form.fill({
  fields: { tenantName: 'John Doe' },
  parties: { tenant: { id: 'tenant-0', name: 'John Doe' } }
})

// Safe variant (returns result object)
const result = form.safeFill({ fields: { tenantName: 'John' } })
if (result.success) {
  const draft = result.data
}
```

### Updating a draft progressively

```ts
// Merge additional data into the draft
const updated = draft.update({
  fields: { moveInDate: '2024-06-01' }
})

// Safe variant
const result = draft.safeUpdate({ fields: { monthlyRent: { amount: 1500, currency: 'USD' } } })
if (result.success) {
  const updated = result.data
}
```

### Querying fill state

`getFillState()` returns a snapshot of progress, including what's open, what's blocked behind conditional visibility, and what to ask next. See [Fill State](/sdk/artifacts/form/fill-state) for full type reference.

```ts
const state = draft.getFillState()

// Progress
state.summary.completionPercent  // 0–100
state.summary.requiredRemaining  // fields/parties still needed

// What to ask next
state.next       // FillTarget | null — first candidate
state.candidates // FillTarget[] — all visible, unfilled targets

// Items blocked by conditional visibility
state.blocked    // FillItemState[] — will unblock when dependencies are filled

// Shorthand helpers
const next = draft.getNextFillTarget()
const all = draft.getAvailableFillTargets()
```

### Acceptance and readiness

`fill()` and `update()` validate every supplied value. Omitted required values remain open so a caller can build a draft across turns. `safeFill()` succeeds when that supplied patch is accepted. Use `draft.isValid()` or `draft.validate()` to assess full readiness, including rules. Rendering a draft is allowed; signing, sealing, and final bundle operations require readiness.

## Extracting data from a filled PDF

`form.extract(pdf, options?)` reads a filled PDF back into form data through a PDF layer's bindings. It reads the PDF's AcroForm field values and maps each one back to its artifact path. The PDF is not changed.

```ts
import { readFileSync } from 'node:fs'
import { w9 } from '@paradoc/essentials'

const { layer, data, report } = await w9.extract(readFileSync('completed-w9.pdf'))

// Extraction does not validate. The normal fill path does.
const result = w9.safeFill(data)
if (result.success) {
  const state = result.data.getFillState() // required values the PDF did not hold are open
}
```

With no `layer` option, `extract` uses the form's only PDF layer. A form with several PDF layers, such as the copies of a 1099, needs `{ layer: 'pdfCopyB' }`. Pass `formatter` when the PDF was filled with a formatter other than the default.

The result has three parts:

| Part             | Description                                                                                                                                        |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `layer`          | The PDF layer the PDF was read against                                                                                                             |
| `data`           | A `{ fields, parties? }` payload with only the values read back exactly                                                                            |
| `report.entries` | One entry for each binding target: `{ path, status, sources, reason? }`. `sources` lists the PDF fields that carry the path with their raw values. |
| `report.unbound` | PDF fields that hold a value no binding covers                                                                                                     |

Each entry's `status` is one of:

* `recovered`: the value was read back exactly and is in `data`.
* `empty`: the PDF fields are empty. An unchecked box counts as empty, because it cannot be told apart from an unanswered one.
* `not_recoverable`: the value cannot be mapped back. A combined binding (`'city,state,zipCode'`) writes several values into one box, and extraction does not guess how to split it; the raw text is in `sources`.
* `unparseable`: the text does not parse into the field's type, or two PDF fields that carry the path disagree.

Direct bindings, enum checkboxes, radio groups, dropdowns, and split fields are reversed exactly. Text is parsed back into the field's type: the formatter's own output for enums, numbers, percentages, dates, and money, as well as plain values a person might type, such as `1234.50` or a date in the locale's numeric order. A money amount needs its currency symbol, or a binding to its `amount` alone. On a layer that prints money without a symbol (`format: { money: { currencyDisplay: 'none' } }`), the amount is read in the currency the field declares; a field with no `currency` is `unparseable`. A structured value written into one box, such as a whole address, is `not_recoverable`; bind its parts to recover them.

A partial value from a combined box stays partial. On the W-9, the city, state, and ZIP code share one box, so `data` has `mailingAddress.line1` only, and `safeFill` names the missing parts.

`extract` throws a `PdfExtractionError` whose `code` names the failure: `no_form_fields` (a flattened or scanned PDF, which needs the hosted Paradoc extraction service), `not_matching` (the PDF has no field for a binding key, so it is not this form), `malformed_pdf` (not a PDF, truncated, or unreadable), `no_pdf_layer`, `layer_required`, `layer_not_found`, `not_pdf_layer`, or `unknown_bindings_source` (the layer's `bindingsFrom` names no layer). An encrypted PDF throws `PdfEncryptedError`, whose `code` is `encrypted_pdf`.

The CLI's [`paradoc data extract`](/cli/commands/data#data-extract) and the AI [`extract`](/ai-tools/extract) tool return the same result.

## Lifecycle Types

The `fill()` method returns a `DraftForm` that follows the form lifecycle:

| Type           | Phase    | Data    | Signatures                    |
| -------------- | -------- | ------- | ----------------------------- |
| `DraftForm`    | Draft    | Mutable | Configure signers/signatories |
| `SignableForm` | Signable | Frozen  | Capture signatures            |
| `ExecutedForm` | Executed | Frozen  | Frozen (complete)             |

### DraftForm

Created by `form.fill()`. Mutable data and signer configuration.

| Name | Type | Description |
| --- | --- | --- |
| `phase` | `'draft'` | Phase discriminator |
| `form` | `Form` | The embedded form definition |
| `data` | `InferFormPayload` | The validated data payload |
| `parties` | `Record<string, Party \| Party[]>` | Party data by role ID |
| `signers` | `Record<string, Signer>` | Global signer registry |
| `signatories` | `Record<string, Record<string, PartySignatory[]>>` | Signatories by role/party |
| `targetLayer` | `string` | Current rendering layer |
| `runtimeState` | `FormRuntimeState` | Evaluated logic state |

**Methods**

| Name | Type | Description |
| --- | --- | --- |
| `getField` | `(fieldId: string) => T \| undefined` | Get a field value |
| `getAllFields` | `() => Record<string, unknown>` | Get all field values |
| `getParty` | `(roleId: string) => RuntimeParty \| RuntimeParty[] \| undefined` | Get party for role |
| `setParty` | `(roleId: string, party: Party \| Party[]) => DraftForm` | Replace the role's parties, validated and given their `<role>-<index>` ids |
| `addParty` | `(roleId: string, party: Party) => DraftForm` | Append a party with the next `<role>-<index>` id |
| `removeParty` | `(roleId: string, index: number) => DraftForm` | Remove a party; later parties take their new ids and keep their signatories |
| `addSigner` | `(signerId: string, signer: Signer) => DraftForm` | Add signer to registry |
| `removeSigner` | `(signerId: string) => DraftForm` | Remove a signer from the registry |
| `addSignatory` | `(roleId, partyId, signatory) => DraftForm` | Link a registered signer to a filled party. Throws for an unknown party or a signer already linked to it |
| `update` | `(patch: Partial<Payload>) => DraftForm` | Merge a patch into current data and return a new DraftForm |
| `safeUpdate` | `(patch: Partial<Payload>) => SafeFillResult` | Safely merge a patch (returns result object) |
| `clear` | `(path: FormPath) => DraftForm` | Remove a field or annex value |
| `reset` | `(path: FormPath) => DraftForm` | Restore a declared default or remove the value |
| `getFillState` | `(options?: FillTargetOptions) => FillState` | Compute full fill state: open/blocked/done items, candidates, progress summary |
| `getNextFillTarget` | `(options?: FillTargetOptions) => FillTarget \| null` | Get the next recommended fill target, or null if all required are done |
| `getAvailableFillTargets` | `(options?: FillTargetOptions) => FillTarget[]` | Get available fill targets in dependency order, required before optional by default |
| `setAnnex` | `(annexId: string, annex: Attachment) => DraftForm` | Set an annex attachment |
| `setTargetLayer` | `(layer: string) => DraftForm` | Change the target layer |
| `prepareForSigning` | `() => SignableForm` | Transition to signable phase |
| `seal` | `(options?: SealOptions) => Promise<SignableForm>` | Render, place signature fields, flatten, and hash the target layer for formal e-signing. See [Seal options](#seal-options) |
| `prepareSeal` | `(options?: SealOptions) => Promise<SealPreparation>` | Resolve the signature map and the converted PDF it describes, without flattening, hashing, or changing phase. Requires a layer with `signatures` |
| `render` | `(options?: RuntimeFormRenderOptions) => Promise<Output>` | Render with embedded data |
| `toJSON` | `() => DraftFormJSON` | Serialize to JSON |
| `toYAML` | `() => string` | Serialize to YAML |
| `clone` | `() => DraftForm` | Create an exact copy |

### SignableForm

Created by `draft.prepareForSigning()` or `draft.seal()`. Frozen data, signature capture allowed.

| Name | Type | Description |
| --- | --- | --- |
| `phase` | `'signable'` | Phase discriminator |
| `isFormal` | `boolean` | True if created via seal() |
| `signatureMap` | `SigningField[] \| undefined` | Signature field coordinates (formal only) |
| `canonicalPdfHash` | `string \| undefined` | PDF hash (formal only) |
| `captures` | `SignatureCapture[]` | Captured signatures |
| `witnesses` | `WitnessParty[]` | Declared witnesses |
| `attestations` | `Attestation[]` | Witness attestations |

**Methods**

| Name | Type | Description |
| --- | --- | --- |
| `captureSignature` | `(role, partyId, signerId, locationId, options?) => SignableForm` | Capture signature at location |
| `captureInitials` | `(role, partyId, signerId, locationId, options?) => SignableForm` | Capture initials at location |
| `addWitness` | `(witness: WitnessParty) => SignableForm` | Add a witness |
| `addAttestation` | `(attestation: Attestation) => SignableForm` | Add attestation |
| `getSignatureStatus` | `(roleId: string) => SignatureStatus` | Get status for role |
| `getOverallSignatureStatus` | `() => OverallSignatureStatus` | Get overall progress |
| `finalize` | `() => ExecutedForm` | Transition to executed phase |

### ExecutedForm

Created by `signable.finalize()`. Fully frozen, ready for archival.

| Name | Type | Description |
| --- | --- | --- |
| `phase` | `'executed'` | Phase discriminator |
| `executedAt` | `string` | ISO timestamp of execution |
| `captures` | `SignatureCapture[]` | Captured signatures |
| `witnesses` | `WitnessParty[]` | Declared witnesses |
| `attestations` | `Attestation[]` | Witness attestations |

**Methods**

| Name | Type | Description |
| --- | --- | --- |
| `getCapture` | `(role, partyId, signerId, locationId, type) => SignatureCapture \| undefined` | Get one capture |
| `getCapturesForLocation` | `(locationId: string) => SignatureCapture[]` | Get captures at a location |
| `getCapturesForParty` | `(roleId: string, partyId: string) => SignatureCapture[]` | Get captures for a party |
| `getCapturesForSigner` | `(signerId: string) => SignatureCapture[]` | Get captures for a signer |
| `render` | `(options?: RuntimeFormRenderOptions) => Promise<Output>` | Render the executed form |
| `toJSON` | `() => ExecutedFormJSON` | Serialize to JSON |
| `toYAML` | `() => string` | Serialize to YAML |
| `clone` | `() => ExecutedForm` | Create an exact copy |

## Render options

`form.render()` takes `RenderOptions`. A filled form's `render()` takes `RuntimeFormRenderOptions`, which is the same without `data`, because the form already holds it.

| Name | Type | Description |
| --- | --- | --- |
| `layer?` | `string` | Layer key to render. Defaults to the target layer, then defaultLayer, then the first layer |
| `renderer?` | `ParadocRenderer` | Override the renderer for this call. Supported MIME types use the built-in renderer by default |
| `renderers?` | `RendererRegistry` | Renderers keyed by layer MIME type, consulted when no `renderer` is given and before the built-in engines. This is how a React layer (`text/tsx`, `text/jsx`) renders |
| `formatter?` | `Formatter` | Formatter used for every value in this render |
| `progressive?` | `FormatterProgressivePolicy` | Text for missing and incomplete values in a preview |
| `bindings?` | `Record<string, string>` | PDF layers only: AcroForm field name to Paradoc path, merged over the layer's own bindings. Refused for any other layer |
| `data?` | `FormData` | Values to render with: field values under `fields`, with `parties`, `annexes` and `defs` beside them. `RenderOptions` only |

```ts
import { reactLayerRenderers } from '@paradoc/react-pdf'

const pdf = await draft.render({
  layer: 'composition',
  renderers: reactLayerRenderers({ components: { 'purchase-order.tsx': PurchaseOrder } }),
})
```

## Seal options

`seal()` renders the target layer, resolves its signature fields, flattens the PDF, and records its hash. A PDF layer seals locally. Any other layer needs an `adapter` that converts it to PDF, unless a renderer registered in `renderers` writes the PDF itself, as the React renderer does.

| Name | Type | Description |
| --- | --- | --- |
| `adapter?` | `SealAdapter` | Converts a non-PDF layer to PDF. Not needed for PDF layers or for a layer whose registered renderer writes PDF |
| `locate?` | `SealLocator` | Resolves anchor positions against the converted PDF when the adapter returns no signature map |
| `formatter?` | `Formatter` | Formatter used on every sealing render pass |
| `renderer?` | `ParadocRenderer` | Renderer override for the sealed layer. Refused with flow placement, because only core can write the flow markers |
| `renderers?` | `RendererRegistry` | Renderers keyed by layer MIME type, the same registry `render` takes |

The seal picks its renderer in this order:

1. A renderer registered for a React layer's MIME type. It receives the flow markers and writes the PDF, so it needs no `adapter`.
2. Core's text renderer, whenever the layer places a slot in flow. Passing `renderer` alongside flow placement throws `SealConfigError`.
3. Otherwise `renderer`, then the `renderers` entry for the layer's MIME type, then the built-in renderer.

An adapter only converts. It implements `convert(request)` and returns `{ pdf, signatureMap? }`. Core still renders, flattens, and computes the SHA-256 hash itself. On a layer with `signatures`, core places every field from its slots and ignores an adapter's `signatureMap`. On a layer with no slots, the adapter's `signatureMap` is the signature map.

```ts
const signable = await draft
  .addSigner('jane', { person: { name: 'Jane Doe' } })
  .addSignatory('tenant', 'tenant-0', { signerId: 'jane' })
  .seal()

signable.signatureMap      // signature field coordinates
signable.canonicalPdfHash  // 'sha256:...'
```

## Related

* [field](/sdk/artifacts/form/field) - Field definitions
* [annex](/sdk/artifacts/form/annex) - Annex slot definitions
* [party](/sdk/artifacts/form/party) - Party role definitions
* [fill-state](/sdk/artifacts/form/fill-state) - Fill state types and progressive filling
* [Form Schema](/schemas/artifacts/form)
