Docuseal Template Model Structure: Understanding Fields, Schema, and Submitters

The Template model in docusealco/docuseal stores fields, schema, and submitters as JSON-serialized text columns, using nested array-of-hash structures to define form inputs, document attachments, and signing parties.

The Template model serves as the core schema definition for multi-party document workflows in the Docuseal open-source repository. It persists three distinct JSON structures within single database rows, enabling flexible, complex form configurations without rigid relational schemas. Understanding how these serialized attributes interact is essential for customizing templates via the API or Ruby SDK.

Database Storage and JSON Serialization

In app/models/template.rb, the three core attributes are defined as string columns with JSON serialization. This design choice allows arbitrary nesting while maintaining relational database compatibility.

attribute :fields,    :string, default: -> { [] }
attribute :schema,    :string, default: -> { [] }
attribute :submitters,:string, default: -> { [{ name: I18n.t(:first_party), uuid: SecureRandom.uuid }] }

serialize :fields,    coder: JSON
serialize :schema,    coder: JSON
serialize :submitters,coder: JSON

Source: [app/models/template.rb](https://github.com/docusealco/docuseal/blob/master/app/models/template.rb#L52-L63)

All three columns use text database types with non-null constraints. The submitters attribute defaults to a single "First Party" entry containing a localized name and generated UUID, ensuring every template initializes with at least one signing role.

The Three Core Structures

The Template model organizes document logic into three distinct domains: input fields, file attachments, and participant definitions.

Fields: Form Input Definitions

The fields array contains hashes representing individual form inputs placed on the document canvas. Each field hash requires specific keys to function within the Docuseal rendering engine:

  • Identifiers: uuid, submitter_uuid (links to the signing party), name
  • Type Configuration: type (text, date, signature, etc.), required, readonly, default_value
  • Display: title, description, prefillable
  • Validation Rules: Nested validation hash with message, pattern, min, max, step
  • Layout: areas array specifying placement via x, y, w, h, page, and attachment_uuid
  • Logic: conditions array for conditional visibility, options array for multi-select fields

Fields reference a submitter_uuid to determine which party completes the input during the signing workflow.

Schema: Document Attachments

The schema array defines static and dynamic documents attached to the template. Each entry represents a file that parties will review or sign:

  • File Sources: attachment_uuid (internal reference), google_drive_file_id (external integration)
  • Metadata: name, dynamic (boolean indicating generated vs. static content)
  • Conditional Logic: conditions array controlling document visibility based on field values

Schema entries are referenced by field areas when placing input fields on specific document pages.

Submitters: Signing Parties

The submitters array defines the sequence and roles of participants:

  • Identity: name, uuid (primary identifier)
  • Workflow: order (integer for sequencing), is_requester (boolean flag)
  • Linking: linked_to_uuid, invite_via_field_uuid (for complex multi-party routing)
  • Invitations: email, invite_by_uuid, optional_invite_by_uuid

The default initializer creates one submitter with I18n.t(:first_party) to support immediate single-party use cases.

Strong Parameters and API Structure

The exact structure expected by controllers is enforced in app/controllers/templates_controller.rb through a comprehensive strong-parameter whitelist:

params.require(:template).permit(
  :name,
  { schema: [[:attachment_uuid, :google_drive_file_id, :name, :dynamic,
              { conditions: [%i[field_uuid value action operation]] }]],
    submitters: [%i[name uuid is_requester linked_to_uuid invite_via_field_uuid
                  invite_by_uuid optional_invite_by_uuid email order]],
    fields: [[:uuid, :submitter_uuid, :name, :type,
              :required, :readonly, :default_value,
              :title, :description, :prefillable,
              { preferences: {},
                default_value: [],
                conditions: [%i[field_uuid value action operation]],
                options: [%i[value uuid]],
                validation: %i[message pattern min max step],
                areas: [%i[uuid x y w h cell_w attachment_uuid option_uuid page]] }]] }
)

Source: [app/controllers/templates_controller.rb](https://github.com/docusealco/docuseal/blob/master/app/controllers/templates_controller.rb#L103-L118)

This whitelist mirrors the JSON schema required for API requests, ensuring that nested hashes for validation rules, conditional logic, and geometric areas pass through to the model.

Practical Implementation Examples

Creating Templates via Ruby SDK

When building templates programmatically, match the structure defined in the controller whitelist:

template = Docuseal::Template.create!(
  name: "Employee Agreement",
  fields: [
    {
      uuid: SecureRandom.uuid,
      submitter_uuid: "party-1-uuid",
      name: "start_date",
      type: "date",
      required: true,
      readonly: false,
      default_value: "",
      title: "Employment Start Date",
      description: "First day of employment",
      prefillable: true,
      preferences: {},
      validation: { pattern: "^\\d{4}-\\d{2}-\\d{2}$", message: "Use YYYY-MM-DD format" },
      areas: [
        { uuid: SecureRandom.uuid, x: 100, y: 200, w: 150, h: 25, page: 1, attachment_uuid: "doc-uuid" }
      ]
    }
  ],
  schema: [
    {
      attachment_uuid: "doc-uuid",
      name: "Contract PDF",
      dynamic: false,
      conditions: []
    }
  ],
  submitters: [
    { name: "HR Representative", uuid: "party-1-uuid", email: "hr@company.com", order: 1 },
    { name: "New Employee", uuid: "party-2-uuid", email: "newhire@example.com", order: 2 }
  ]
)

API JSON Payload Structure

For direct API integration, POST payloads follow the same nested structure:

{
  "template": {
    "name": "Service Agreement",
    "fields": [
      {
        "uuid": "field-123",
        "submitter_uuid": "submitter-456",
        "name": "service_type",
        "type": "select",
        "required": true,
        "options": [
          { "value": "Consulting", "uuid": "opt-1" },
          { "value": "Development", "uuid": "opt-2" }
        ],
        "areas": [{ "x": 50, "y": 100, "w": 200, "h": 30, "page": 1 }]
      }
    ],
    "schema": [
      {
        "attachment_uuid": "att-789",
        "name": "Agreement Template",
        "dynamic": true,
        "conditions": [
          { "field_uuid": "field-123", "value": "Consulting", "action": "show", "operation": "is" }
        ]
      }
    ],
    "submitters": [
      {
        "name": "Service Provider",
        "uuid": "submitter-456",
        "email": "vendor@example.com",
        "is_requester": true
      }
    ]
  }
}

Accessing Serialized Data

When working with existing templates in Ruby, access the deserialized structures directly:

template = Template.find(42)

# Map fields to their assigned submitters

template.fields.each do |field|
  puts "Field '#{field['name']}' assigned to submitter #{field['submitter_uuid']}"
end

# Build submitter lookup by UUID

submitter_by_uuid = template.submitters.index_by { |s| s['uuid'] }
first_party = submitter_by_uuid[template.fields.first['submitter_uuid']]

Summary

  • The Template model uses JSON serialization to store flexible, nested data structures in text database columns.
  • Fields define form inputs with validation, placement areas, and conditional logic, referencing specific submitters via UUID.
  • Schema manages document attachments, supporting both static files and dynamic content generation with visibility conditions.
  • Submitters maintain party metadata including sequencing, email routing, and inter-party linkage.
  • The strong-parameter whitelist in templates_controller.rb documents the exact API contract for creating and updating templates.

Frequently Asked Questions

How does the Template model handle complex nested data without database migrations?

The Template model utilizes ActiveRecord's serialize macro with JSON coding, as defined in lines 59-63 of app/models/template.rb. This stores Ruby hashes and arrays as JSON text in the database, allowing the schema to evolve by updating the application code rather than running database migrations.

What connects a field to a specific signing party?

Each field hash contains a submitter_uuid key that must match the uuid of an entry in the submitters array. When rendering the document, Docuseal filters fields by matching these UUIDs to display only the relevant inputs to each party during their signing step.

Can schema attachments be dynamically generated rather than static files?

Yes. The schema array supports dynamic: true entries, which indicate documents generated at runtime rather than pre-uploaded PDFs. Dynamic schema entries can reference the same attachment_uuid used in field areas, allowing programmatic insertion of content before signing.

Where is the complete structure for API requests documented?

The definitive reference for API JSON structures is the strong-parameter whitelist in app/controllers/templates_controller.rb lines 103-118. Additionally, the docs/api/ directory in the repository contains TypeScript, Ruby, and Node.js examples showing practical implementations of the fields, schema, and submitters arrays.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →