The Data-First Principle

Most agreement platforms start with a document. You upload a PDF, drag fields onto it, and write backend code to fill in values from your application. SignStack starts with your data.

Your application already has structured business objects — employees, deals, clients, invoices. SignStack treats these as the source of truth. Documents are just one possible rendering of that data.

The Traditional Approach

Here's what document-first looks like:

  1. Upload a PDF of your offer letter
  2. Manually place 20+ fields at pixel coordinates (name at x:150 y:280, salary at x:150 y:340...)
  3. Write backend code that maps your employee object to those field positions
  4. Hope the mapping doesn't break when someone updates the PDF layout
  5. Repeat for every new document type

The document is the center of gravity. Your data is secondary — something you shoehorn into a pre-existing layout.

The SignStack Approach

Data-first flips this entirely:

  1. Define your data — a JSON Schema for employee, company, position
  2. Keep your existing PDF — the same one you'd upload to a traditional tool
  3. Create a Template primitive — declare inputs, then attach a value expression to each PDF field that pulls from entity data
  4. Data flows in, documents flow out — change the schema, the document adapts
# The Template connects data to PDF fields
spec:
  type: pdf
  content: offer_letter_pdf@1.0.0

  inputs:
    - key: employee
      schema: employee@1.0.0
    - key: position
      schema: position@1.0.0

  fields:
    - key: employee_name
      type: text_field
      value:
        expr: "$.employee.firstName & ' ' & $.employee.lastName"
    - key: salary
      type: text_field
      value:
        expr: "'
& $formatNumber($.position.salary, '#,##0.00')" - key: start_date type: text_field value: expr: "$fromMillis($toMillis($.position.startDate), '[MNn] [D1], [Y0001]')"

SignStack also supports HTML/Handlebars templates alongside PDFs. See Templates.

The same employee and company schemas power your offer letter, NDA, and benefits form. No duplication. No drift.

Entities: Your Business Data

At the heart of SignStack are Entities — structured JSON objects representing your core business concepts. An employee entity, a deal_info entity, a company entity. These are the same data structures you already work with in your application.

You define the shape of your entities using Schemas (built on the JSON Schema standard):

apiVersion: signstack/v1beta2
kind: Schema

metadata:
  key: employee
  version: 1.0.0
  name: Employee

spec:
  jsonSchemaDraft: 2020-12
  file: ./definitions/employee.json

When a workflow runs, every piece of entity data is validated against its schema. Invalid data is rejected before it ever reaches a document.

The Entity Context

For any running Workflow, the collection of all entity instances forms the Entity Context — the single source of truth for that transaction. Every expression in the system reads from this context.

SignStack uses JSONata as its expression engine to make documents and workflows react to this data.

Expressions: Data Drives Everything

In SignStack, data isn't just an input — it drives every aspect of the system.

Field values are expressions over your data:

value:
  expr: "$.employee.firstName & ' ' & $.employee.lastName"

Visibility is a function of your data:

# Only show the manager approval field if deal exceeds threshold
includeIf: '$.deal.requestedDiscount > $.user.discountLimit'

Required-ness adapts to your data:

# Manager input only required for senior roles
inputs:
  - key: manager
    schema: employee@1.0.0
    required:
      expr: '$.position.level > 3'

Workflow shape adapts to your data:

# Document only included for enterprise customers
documents:
  - key: enterprise_addendum
    template: addendum@1.0.0
    includeIf: "$.client.tier = 'Enterprise'"

# Step only runs for large deals
- key: legal_review
  type: participant
  participant: legal_counsel
  includeIf: '$.deal.value > 100000'

The runtime workflow that ships is a function of your entity data — different inputs, different documents, different steps.

Participants are resolved from your data:

participants:
  - key: new_hire
    name:
      expr: "$.employee.firstName & ' ' & $.employee.lastName"
    email:
      expr: '$.employee.email'

Workflow progression updates your data:

onComplete:
  updates:
    - key: employee
      transform: |
        {
          "acceptedAt": $now(),
          "status": "offer_accepted"
        }

Evolving Data

As a workflow progresses, entity data can evolve. When a participant completes a signing step, onComplete transforms modify the Entity Context. Downstream steps see the updated data automatically.

SignStack maintains an immutable history of every data change, linked to the step that caused it. This provides a complete audit trail — you can always trace exactly when and why data changed.

Why This Matters

One data model, many documents. The same employee schema powers your offer letter, NDA, benefits enrollment, and any future document. Define once, render everywhere.

No custom backend code. Business logic lives in your YAML primitives — expressions, transforms, conditions — not scattered across your application codebase.

Schema validation at every stage. Invalid data never enters a workflow. Schemas are enforced when workflows start and when onComplete transforms run.

AI-native. Schemas turn every workflow into a typed contract. AI agents can run your workflows reliably — inputs are validated at the boundary, outputs are structured, errors are caught instead of silently corrupting downstream state.

Auditable by design. Versioned data history with every change linked to the step, participant, and timestamp that caused it. Built for compliance.