OpenForm
SDKArtifactsform

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 = open.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, witnesses: 1 } }
  },
  annexes: {
    photoId: { title: 'Photo ID', required: true }
  },
  layers: {
    html: { kind: 'inline', mimeType: 'text/html', text: '<p>Tenant: {{tenantName}}</p>' }
  },
  defaultLayer: 'html'
})

// Builder pattern
const form = open.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, witnesses: 1 }).build())
  .annex('photoId', annex().title('Photo ID').required().build())
  .inlineLayer('html', { mimeType: 'text/html', text: '<p>Tenant: {{tenantName}}</p>' })
  .defaultLayer('html')
  .build()

Adding instructions

// Inline instructions
const form = open.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 = open.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 = open.form.from(jsonData)

// Safe parsing (returns result object)
const result = open.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' },
  parties: {
    tenant: { id: 'tenant-0', name: 'John Doe' },
    landlord: { id: 'landlord-0', name: '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' })

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

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

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

// 6. Render the executed form
const output = await executed.render({ renderer: textRenderer })

API

Object Pattern

open.form(input: FormInput): FormInstance

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
Internal code or reference number
releaseDate?: string
ISO date when artifact was released
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
Internal code
releaseDate: string | undefined
ISO date string
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<unknown>
Validate one annex input key/value
validateAnnexesPatch: (annexes: unknown) => ProgressiveValidationResult<Record<string, unknown>>
Validate a partial annexes patch
fill: (data: FillOptions) => DraftForm
Create a DraftForm with validated data
safeFill: (data: FillOptions) => Result<DraftForm>
Create DraftForm (returns result object)
partialFill: (seed?: Partial<Payload>, options?: PartialFillOptions) => DraftForm
Create a DraftForm from partial or empty data for progressive filling
safePartialFill: (seed?: Partial<Payload>, options?: PartialFillOptions) => SafePartialFillResult
Safely create a DraftForm from partial data (returns result object)
render: (options: RenderOptions) => Promise<Output>
Render using a renderer
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.

open.form()
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
releaseDate: (value: string) => FormBuilder
Set release date (ISO format)
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
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: () => FormInstance
Build and validate

Static Methods

Parse forms from unknown data sources:

from: (input: unknown) => FormInstance
Parse unknown input (throws on error)
safeFrom: (input: unknown) => 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' },
})

When allowAdditionalAnnexes is false, unknown annex keys are rejected.

Incremental Filling

Use partialFill 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.partialFill()

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

// Safe variant (returns result object)
const result = form.safePartialFill({ 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()

Validation modes

Both partialFill and update accept a validate option:

ModeBehavior
"patch" (default)Validate only the provided fields
"full"Validate entire payload including required fields
"none"Skip validation entirely
// Skip validation (useful when data is pre-validated)
const draft = form.partialFill(data, { validate: 'none' })

// Full validation (same as fill())
const draft = form.partialFill(data, { validate: 'full' })

// Also evaluate rules
const draft = form.partialFill(data, { validate: 'patch', rules: true })

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() or form.partialFill(). 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
setField: (fieldId: string, value: T) => DraftForm
Update a field (immutable)
updateFields: (partial: Record<string, unknown>) => DraftForm
Update multiple fields
getParty: (roleId: string) => Party | Party[] | undefined
Get party for role
setParty: (roleId: string, party: Party | Party[]) => DraftForm
Set party (immutable)
addParty: (roleId: string, party: Party) => DraftForm
Add party to role
addSigner: (signerId: string, signer: Signer) => DraftForm
Add signer to registry
addSignatory: (roleId, partyId, signatory) => DraftForm
Link signer to party
update: (patch: Partial<Payload>, options?: UpdateOptions) => DraftForm
Merge a patch into current data and return a new DraftForm
safeUpdate: (patch: Partial<Payload>, options?: UpdateOptions) => SafePartialFillResult
Safely merge a patch (returns result object)
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 all available fill targets in declaration order
prepareForSigning: () => SignableForm
Transition to signable phase
seal: (adapter: Sealer) => Promise<SignableForm>
Seal for formal e-signing
render: (options: RenderOptions) => Promise<Output>
Render with embedded data
toJSON: () => DraftFormJSON
Serialize to JSON
toYAML: () => string
Serialize to YAML

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
Methods
getCaptures: () => SignatureCapture[]
Get all captures
getWitnesses: () => WitnessParty[]
Get all witnesses
getAttestations: () => Attestation[]
Get all attestations
render: (options: RenderOptions) => Promise<Output>
Render the executed form
toJSON: () => ExecutedFormJSON
Serialize to JSON

Related

On this page