Legal contract-signing

Contract Approval Pipeline

Every contract goes through Legal before execution. Large contracts or long-term agreements add Finance and Board levels. Either legal approver can clear the review. For board-level contracts, both CEO and board chair must sign.

Under $50k
Dept Head
→
Legalany one
→
Signed
$50k – $250k
Dept Head
→
Legal
→
CFO
→
Signed
Over $250k
Dept Head
→
Legal
→
CFO
→
CEO + Boardboth required
→
Signed

Legal always gates execution

Legal review is a fixed level 2 in every contract chain. It uses mode: 'any' — either counsel or reviewer can clear it. Board-level contracts use mode: 'all' — both CEO and board chair must sign.

  • Legal review cannot be skipped for any contract type
  • High-value or long-term adds CFO automatically
  • Board approval requires both signatories (mode: 'all')
  • The counterparty's signature is a final level, resolved from the contract data
contract-template.ts
// The counterparty's signer comes from the contract data
engine.registerResolver('counterparty-signatory', (_submittedBy, data) => String(data.counterpartySignerId));

const totalValue = (min: number) => ({ field: 'totalValue', operator: '>', value: min });

await engine.defineTemplate({
  name: 'contract-signing',
  documentType: 'contract',
  levels: [
    { level: 1, name: 'Department head', approvers: [{ type: 'role', role: 'department-head' }], mode: 'any' },
    // Legal review is part of every chain; either counsel can clear it
    {
      level: 2,
      name: 'Legal',
      approvers: [{ type: 'role', role: 'legal-counsel' }, { type: 'role', role: 'legal-reviewer' }],
      mode: 'any',
    },
  ],
  conditions: [
    // Finance for high-value or long-duration contracts
    {
      when: { any: [totalValue(50_000), { field: 'durationMonths', operator: '>', value: 24 }] },
      addLevels: [{ level: 3, name: 'CFO', approvers: [{ type: 'role', role: 'cfo' }], mode: 'any' }],
    },
    // Board for very high-value contracts: both must sign
    {
      when: totalValue(250_000),
      addLevels: [
        {
          level: 4,
          name: 'Board',
          approvers: [{ type: 'role', role: 'ceo' }, { type: 'role', role: 'board-chair' }],
          mode: 'all',
        },
      ],
    },
    // The counterparty signs last
    {
      when: { field: 'requiresCounterSignature', operator: '==', value: true },
      addLevels: [
        { level: 5, name: 'Counterparty', approvers: [{ type: 'dynamic', resolver: 'counterparty-signatory' }], mode: 'any' },
      ],
    },
  ],
});

Contract workflows have unusual edge cases.

Unlike expense approvals, contracts involve external parties and evolve over their lifecycle.

01

Amendment restarts the chain

A $45k contract is in approval. The vendor negotiates an extension to $60k. The amendment crosses the CFO threshold — it needs a new approval chain, not just a note on the old one.

amendment.ts
// Withdraw the in-flight contract, then submit the amended one
async function submitAmendment(original: Contract, amended: Contract) {
  await engine.cancel(original.instanceId, {
    cancelledBy: amended.submittedBy,
    reason: 'Superseded by amendment',
  });

  // The new instance evaluates the conditions against the amended value:
  // 45k -> 60k adds the CFO level
  return engine.submit({
    templateName: 'contract-signing',
    documentType: 'contract',
    documentId: `${original.contractId}-A1`,
    submittedBy: amended.submittedBy,
    data: { ...amended.data, isAmendment: true, originalContractId: original.contractId },
  });
}

The original instance ends as cancelled with the reason in its audit trail. The new instance carries originalContractId in its data, and the thresholds apply to the amended value.

02

External counter-signature via webhook

The counterparty (a vendor or customer) must also sign before the contract is fully executed. Their signature comes from an e-signature platform, not from inside your system.

countersign-webhook.ts
// Receive the counter-signature from DocuSign / Adobe Sign
app.post('/webhooks/docusign', async (req, res) => {
  const { envelopeId, status, signerEmail } = req.body;
  if (status !== 'completed') return res.sendStatus(200);

  // Your own table maps the envelope to the approval instance
  const { instance_id } = (await db.query(
    'SELECT instance_id FROM contract_envelopes WHERE envelope_id = $1',
    [envelopeId]
  )).rows[0];

  // signerEmail must match the id the resolver returned: `external:${email}`
  await engine.approve(instance_id, {
    approverId: `external:${signerEmail}`,
    comment: `Counter-signed via DocuSign envelope ${envelopeId}`,
  });

  res.sendStatus(200);
});

The counterparty is an approver like any other: the resolver returns an id prefixed with external:, and the webhook approves as that id. The audit trail records who signed externally.

03

Legal hold mid-approval

A contract is at the CFO level awaiting approval when legal sends a hold because of a regulatory review. The CFO should not be able to approve until the hold is lifted.

1 Legal sets a hold flag on the contract in your DB
2 Pass an authorizationPolicy whose authorize() returns a message for approve while the hold flag is set
3 When legal lifts the hold, the CFO can approve normally — the approval history is unaffected

IAuthorizationPolicy is called on approve, reject and the other level actions. Returning a non-empty string denies the call with an ApprovalForbiddenError carrying that message, and the instance is left unchanged.

Your next approval workflow
ships faster than the last one.

Drop it into any Node.js project. No database required for local development.

$ npm install hierarchical-approval

Peer dep for Postgres: npm install pg @types/pg