Form Builder

Last updated on

Create Form artifacts with fields, parties, and signing flows

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

Examples

Creating a form

// 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

// 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

// 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.

// 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

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.

Parameters

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:

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
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:

All methods return FormBuilder and are chainable.

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

Static Methods

Parse forms from unknown data sources. Both accept an ArtifactInstanceOptions to bind a resolver for any file-backed layer:

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:

// 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:

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

Field input validation

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

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

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: { 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

// 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

// 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 for full type reference.

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.

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:

PartDescription
layerThe PDF layer the PDF was read against
dataA { fields, parties? } payload with only the values read back exactly
report.entriesOne entry for each binding target: { path, status, sources, reason? }. sources lists the PDF fields that carry the path with their raw values.
report.unboundPDF 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 and the AI extract tool return the same result.

Lifecycle Types

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

TypePhaseDataSignatures
DraftFormDraftMutableConfigure signers/signatories
SignableFormSignableFrozenCapture signatures
ExecutedFormExecutedFrozenFrozen (complete)

DraftForm

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

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

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

phase: 'executed'
Phase discriminator
executedAt: string
ISO timestamp of execution
captures: SignatureCapture[]
Captured signatures
witnesses: WitnessParty[]
Declared witnesses
attestations: Attestation[]
Witness attestations
Methods
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.

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

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.

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:...'

On this page