Build a library import file
A drafting library moves between workspaces as one file: a zip holding two JSON files. Export on a document type's card writes it, and Import a library reads it. Inside, everything refers to everything else by its key, never by an id from a workspace, so you can write the file yourself: turn a precedent your firm already uses into a first library, restructure a library in a text editor, or keep a copy of your library with your other precedents.
Nothing an import creates is used until you say so. Every block arrives as a draft, and the document type and its templates arrive restricted to you, so a file you have written can be imported, checked and corrected before anyone drafts from it.
Before you start
- Importing needs the author documents grant (admins have it) and a plan that includes document drafting.
- You need a text editor that saves UTF-8, and a way to make a zip (make the zip shows how).
- A block's text is written in the block language. Keep the block language reference open while you write.
- A complete file to copy from helps. The example on this page is short; the feature tour example uses every feature of a document type and its blocks. Unzip either to see a whole file.
What's in the file
engagement-letter.zip
├── manifest.json
└── library/
└── library.json
- manifest.json says what the file is. It sits at the top of the zip.
- library/library.json holds the library: one document type, its fields, its blocks, its templates and its selection tests. It sits in a folder named
library.
Both are JSON in UTF-8, and the names of both files and the folder are exact. Write the property names as this page shows them. The import ignores a property it doesn't recognise, so a misspelt one is quietly treated as missing: check the file against the schema before you import it.
The manifest
| Property | Type | Required | What it holds |
|---|---|---|---|
formatVersion |
number | Yes | 1, the current format. A higher number is refused as coming from a newer version of Ferrith. |
kind |
text | Yes | library. |
name |
text | Yes | The library's name, shown when you import it. |
exportedAtUtc |
date and time | No | When the file was written, for your own records. |
entityNames |
list of text | No | The names of the document type and its templates, for your own records. |
notices |
list of text | No | Notes shown to the person importing, before and after the import: what to check, what to change first. |
The library
library/library.json is one object with five lists:
| Property | What it holds |
|---|---|
documentTypes |
The document type, as a list of one. Required. |
fields |
The questions the type's documents ask. |
blocks |
The building blocks: their text and the rules between them. |
templates |
Fixed recipes of blocks for the type. |
selectionTests |
Briefs that measure how well the AI picks blocks for the type. |
A property you leave out takes its default: an empty list, no value, or false. Keys come in two shapes:
- A field key is lower-case words joined by dots and underscores, at most six words and 64 characters:
client.full_name. The dots only group related fields. The first word can't be one of the block language's own names:document,page,money,words,ordinal,format_date,names,list,ref,image,anchor,page_break,string,array,math,date,object,regex,timespan,html,if,else,end,for,in,while,case,when,func,ret,with,wrap,capture,include,import,readonly,true,false,null,empty,tablerow,break,continue,thisorloop. - Every other key, for a document type, a section, a field group or a block, is lower-case letters, digits and hyphens, at most 64 characters:
engagement-letter.
The document type
| Property | Type | Required | What it holds |
|---|---|---|---|
key |
key | Yes | The type's key. |
name |
text | Yes | The name members see. |
description |
text | No | What the type is for. |
numbering |
text | Yes | The clause numbering: continuous (1, 2, 3 through the document), by_section (3.1, 3.2, restarting in each section) or none. |
listNumbering |
text | No | plain (a block's numbered lists print as written; the default) or legal (they print as sub-paragraphs under the clause: 4.7.1, then (a), then (i)). |
numberSectionHeadings |
true / false | No | With numbering set to by_section, true puts a numbered section's number in its heading: "3. Letting and rent" over clauses 3.1 and 3.2. |
sectionHeadingLevel |
whole number | No | How large the section headings print, from 1 (the size of the document's title) to 4; a schedule's parts print one size smaller. Left out, 2. |
sections |
list | No | The headings, in order. See below. |
mandatoryBlockKeys |
list of keys | No | The blocks every document of the type includes. |
fieldGroups |
list | No | The headings the form asks its questions under. See below. |
titlePattern |
text | No | The document's title, with merge fields: Engagement letter for {{ client.name }}. |
hideTitle |
true / false | No | true leaves the title line out of the document. |
titleAlignment |
text | No | left (the default), centre or right. |
header, footer |
text | No | Printed on every page of an export, written in the block language. {{ page.number }} and {{ page.count }} print the page numbers; a cross-reference can't be used. |
headerOnFirstPage |
true / false | No | true prints the header on the first page too. Left out, the first page has no header. |
selectionGuidance |
text | No | Guidance for the AI when it proposes blocks for the type. |
draftBanner |
true / false | No | true marks the text as a draft until the document is reviewed. |
requireReviewBeforeExport |
true / false | No | true holds every export until someone marks the document reviewed. |
reviewers |
list | No | Who may review: admins, owner or both. Named people and roles can't travel in a file; add them in the library after the import. |
Each section:
| Property | Type | Required | What it holds |
|---|---|---|---|
key |
key | Yes | Blocks name the section they sit under by this key. |
title |
text | Yes | The heading. |
numbered |
true / false | No | true numbers the blocks in the section; a schedule's or a part's are numbered from 1. Left out, they print without numbers. |
hideTitle |
true / false | No | true leaves the heading out, for a section whose blocks supply their own. |
kind |
text | No | clause (the default), schedule (labelled Schedule 1, 2 as it prints) or part (a part of a schedule, labelled Part 1, 2 under it). |
parentKey |
key | For a part | The key of the schedule the part belongs to, listed above it. |
newPage |
true / false | No | true starts the section on a new page in the PDF and Word exports. |
Each field group:
| Property | Type | Required | What it holds |
|---|---|---|---|
key |
key | Yes | The group's key. |
title |
text | Yes | The heading on the form. |
fieldKeys |
list of field keys | Yes | The questions under the heading, in order. Each must be a field in the file, or one your workspace already has, and a field can be in one group only. |
Fields
| Property | Type | Required | What it holds |
|---|---|---|---|
key |
field key | Yes | How blocks merge the answer: {{ client.name }}. |
label |
text | Yes | The question, as the form shows it. |
help |
text | No | A line of help under the question. |
kind |
text | Yes | text, textarea (long text), date, integer (a whole number), number, money, boolean (yes / no), choice (one of a list), choices (several from a list), person, persons (people), address or image. |
options |
list of text | For choice and choices |
The answers to choose from: 1 to 200, each different. |
validation |
object | No | Rules for the answer. See below. |
documentTypeKeys |
list of keys | No | The document types the field belongs to. An empty list makes it available to every document type in the workspace. |
defaultValueJson |
text | No | A default answer for every new document. See below. |
allowUserChange |
true / false | No | With a default: true lets the person drafting change it; false, or left out, makes it fixed, read-only on the form. |
imageWidthMm |
number | No | For an image: the printed width in millimetres, 5 to 170. Left out, 40. |
The rules in validation are all optional:
| Property | For | What it sets |
|---|---|---|
minLength, maxLength |
text, long text, address | The shortest and longest answer, in characters. |
min, max |
whole number, number, money | The lowest and highest answer. |
pattern |
text, long text, address | A regular expression the answer must match. It matches anywhere in the answer: start it with ^ and end it with $ to test the whole answer. |
minItems, maxItems |
several from a list, people | The fewest and most answers. |
A default answer is written as JSON and then stored as a string, so a text default keeps its own quotation marks, escaped with a backslash. It must be a valid answer to the field and pass the field's rules; an invalid default refuses the import.
| Kind | Write the default as | defaultValueJson |
|---|---|---|
| text, long text, address | text; \n between an address's lines |
"\"Example Firm LLP\"" |
| date | year, month and day | "\"2026-10-01\"" |
| whole number, number, money | a number | "30", "1500.50" |
| yes / no | true or false |
"true" |
| one of a list | one of the options | "\"Fixed fee\"" |
| several from a list | a list of options | "[\"Email\", \"Post\"]" |
| person | a name and an address | "{\"full_name\": \"Jane Smith\", \"address\": \"1 High Street\"}" |
| people | a list of persons | "[{\"full_name\": \"Jane Smith\"}]" |
| image | nothing: pictures don't travel in a file | null; upload the default on the field after the import |
A text, long text, address, date or one-of-a-list default written without the inner quotation marks, "Fixed fee", is read as text too.
Blocks
| Property | Type | Required | What it holds |
|---|---|---|---|
key |
key | Yes | How other blocks, templates and tests name the block. |
title |
text | Yes | The block's title in the library. |
description |
text | No | What the block says. The AI reads it, with useWhen, when it proposes blocks: write both for a colleague who has never seen the block. |
useWhen |
text | No | When to choose the block, and over which alternative. |
script |
text | Yes | The block's text in the block language. A new line in the text is a new line in the document; in JSON, write it as \n. |
guidance |
text | No | Guidance for the person drafting, shown with the block. |
placements |
list | To publish | Where the block sits in each document type. See below. A block with no placement imports but can't be published. |
includeWhen |
text | No | The plan's test over the answers. A yes / no compares with `true` or `false`, a number with the number in backticks, text with text in single quotes: matter.fee_basis == 'Fixed fee'. |
auto |
true / false | No | true adds the block to the plan by itself when includeWhen holds. |
variantGroup |
text | No | Blocks in the same group are alternatives; a plan takes at most one. |
requiresKeys, conflictsKeys |
lists of keys | No | Blocks that must be, or can't be, in the same document. Each must be a block in the file. |
defines, uses |
lists of text | No | Defined terms, so a block that uses a term brings in the block that defines it. |
optionalFieldKeys |
list of field keys | No | Fields the block merges that may be left unanswered. |
unnumbered |
true / false | No | true prints the block without a clause number: a heading, a recital, an address. |
tags |
list of text | No | Labels the Blocks tab's search finds, which also help the AI find the block in a large library. |
The block's text and its includeWhen are written differently: a condition inside the text compares text in double quotes ({{ if matter.fee_basis == "Fixed fee" }}), the plan's test in single quotes.
Each placement:
| Property | Type | Required | What it holds |
|---|---|---|---|
documentTypeKey |
key | Yes | The document type. |
section |
key | Yes | The key of one of that type's sections; empty when the type has none. |
order |
number | Yes | The block's position in the section, lowest first. |
Templates
| Property | Type | Required | What it holds |
|---|---|---|---|
documentTypeKey |
key | Yes | The document type in the file. |
name |
text | Yes | The template's name. |
description |
text | No | What the template is for. |
blockKeys |
list of keys | Yes | The blocks a document starts with, beside the ones the type always includes. |
requireReviewBeforeExport |
true / false | No | true holds every export of a document made from the template until someone marks it reviewed. |
Selection tests
| Property | Type | Required | What it holds |
|---|---|---|---|
documentTypeKey |
key | Yes | The document type in the file. |
name |
text | Yes | The test's name. |
brief |
text | Yes | A brief written the way a member would describe the document. |
expectedBlockKeys |
list of keys | No | The blocks a right answer must pick. |
forbiddenBlockKeys |
list of keys | No | The blocks a right answer must not pick. |
What the import checks
Before it writes anything, the import reads the whole file and refuses it, with a message, when:
- The zip is larger than 32 MB, or
library.jsonis larger than 16 MB. manifest.jsonorlibrary/library.jsonis missing, or either isn't valid JSON.kindisn'tlibrary, orformatVersionis higher than 1.library.jsonholds more than 2,000 blocks or 2,000 fields.- There is no document type; a document type, field or block has no valid key, or two share one; a document type has no name, a field no label or no valid kind, a block no title or text, a template no name, or a selection test no name or brief.
- Its templates would take the workspace past its limit of templates.
Then it lists every field key your workspace already uses, and you choose for each: use the existing field or create a new one. A document type or block key your workspace already uses is given a suffix, engagement-letter-2, and the references to it are updated.
While it writes, a problem refuses the import and removes everything written so far:
- a document type's numbering, title alignment, list numbering, section heading size, sections or field groups aren't valid, or its header or footer has an error;
- a field's options, rules, default or image width aren't valid;
- the import would take the workspace past its limit of document types or building blocks.
A block's text, its includeWhen and the sections its placements name never refuse an import: they are checked when you publish the block. A block whose text has an error arrives marked Errors; the block editor explains each error and you fix it there.
Ignored without a message: a key in mandatoryBlockKeys, requiresKeys, conflictsKeys, a template's blockKeys or a test's block lists that names no block in the file; a placement, template or test that names a document type not in the file; reviewers other than admins and owner; any property the import doesn't recognise.
An import always adds. It never changes a document type or block your workspace already has: importing a changed file into the workspace it came from adds a second copy, with suffixed keys. To change a library in place, edit it on the library page.
A complete example
A short engagement letter: one document type with three sections, seven fields, six blocks (two of them alternatives, added by the answer to one question), a template and a selection test. Save the two files under the names shown, zip them and import the zip.
manifest.json:
{
"formatVersion": 1,
"kind": "library",
"name": "Engagement letter (example)",
"exportedAtUtc": "2026-10-02T09:00:00Z",
"entityNames": ["Engagement letter (example)", "Standard engagement"],
"notices": ["An example for learning the drafting library, not suitable for real-world use."]
}
library/library.json:
{
"documentTypes": [
{
"key": "engagement-letter",
"name": "Engagement letter (example)",
"description": "An example for learning the drafting library, not suitable for real-world use.",
"sections": [
{ "key": "opening", "title": "Opening", "hideTitle": true },
{ "key": "terms", "title": "Terms of engagement", "numbered": true },
{ "key": "closing", "title": "Closing", "hideTitle": true }
],
"numbering": "continuous",
"mandatoryBlockKeys": ["opening", "scope", "sign-off"],
"fieldGroups": [
{ "key": "client", "title": "The client", "fieldKeys": ["client.name", "client.address"] },
{
"key": "matter",
"title": "The matter",
"fieldKeys": ["matter.description", "matter.start_date", "matter.fee_basis", "matter.fixed_fee"]
}
],
"selectionGuidance": "Choose the fee block that matches how the brief says the client will be charged.",
"titlePattern": "Engagement letter for {{ client.name }}",
"header": "**Example document, not suitable for real-world use**",
"headerOnFirstPage": true,
"footer": "{{ firm.name }}, page {{ page.number }} of {{ page.count }}",
"draftBanner": true,
"reviewers": ["admins"]
}
],
"fields": [
{ "key": "firm.name", "label": "Firm name", "kind": "text", "defaultValueJson": "\"Example Firm LLP\"" },
{ "key": "client.name", "label": "Client name", "kind": "text", "documentTypeKeys": ["engagement-letter"] },
{ "key": "client.address", "label": "Client address", "kind": "address", "documentTypeKeys": ["engagement-letter"] },
{
"key": "matter.description",
"label": "What we will advise on",
"help": "Completes the sentence: We will advise you on…",
"kind": "textarea",
"documentTypeKeys": ["engagement-letter"]
},
{ "key": "matter.start_date", "label": "Start date", "kind": "date", "documentTypeKeys": ["engagement-letter"] },
{
"key": "matter.fee_basis",
"label": "Fee basis",
"kind": "choice",
"options": ["Fixed fee", "Hourly rates"],
"documentTypeKeys": ["engagement-letter"],
"defaultValueJson": "\"Fixed fee\"",
"allowUserChange": true
},
{
"key": "matter.fixed_fee",
"label": "Fixed fee",
"help": "Before VAT.",
"kind": "money",
"validation": { "min": 1 },
"documentTypeKeys": ["engagement-letter"]
}
],
"blocks": [
{
"key": "opening",
"title": "Opening",
"description": "The client's name and address, the greeting and the opening sentence.",
"useWhen": "Every engagement letter.",
"script": "{{ client.name }}\n{{ client.address }}\n\nDear {{ client.name }},\n\nThank you for instructing us. This letter sets out the terms on which we will act for you.",
"placements": [{ "documentTypeKey": "engagement-letter", "section": "opening", "order": 1 }],
"unnumbered": true
},
{
"key": "scope",
"title": "Scope of the work",
"description": "What we will advise on and when the work starts. Defines the Services.",
"useWhen": "Every engagement letter.",
"script": "**Scope.** We will advise you on {{ matter.description }} (the \"Services\"), starting on {{ matter.start_date | format_date \"d MMMM yyyy\" }}.",
"defines": ["Services"],
"placements": [{ "documentTypeKey": "engagement-letter", "section": "terms", "order": 1 }]
},
{
"key": "fees-fixed",
"title": "Fees: a fixed fee",
"description": "A fixed fee for the Services, plus VAT.",
"useWhen": "The client pays a fixed fee, not by the hour.",
"script": "**Fees.** Our fee for the Services is a fixed {{ matter.fixed_fee | money }} plus VAT.",
"includeWhen": "matter.fee_basis == 'Fixed fee'",
"auto": true,
"variantGroup": "fees",
"uses": ["Services"],
"placements": [{ "documentTypeKey": "engagement-letter", "section": "terms", "order": 2 }]
},
{
"key": "fees-hourly",
"title": "Fees: hourly rates",
"description": "Fees charged for the time spent, at hourly rates confirmed in writing.",
"useWhen": "The client is charged by the hour, not a fixed fee.",
"script": "**Fees.** We charge for the time we spend on the Services at our hourly rates, which we will confirm to you in writing.",
"includeWhen": "matter.fee_basis == 'Hourly rates'",
"auto": true,
"variantGroup": "fees",
"uses": ["Services"],
"placements": [{ "documentTypeKey": "engagement-letter", "section": "terms", "order": 3 }]
},
{
"key": "complaints",
"title": "Complaints",
"description": "How the client can complain about the Services.",
"useWhen": "The letter should tell the client how to complain, as most should.",
"script": "**Complaints.** If you are unhappy with the Services described in clause {{ ref \"scope\" }}, please tell us and we will try to put things right.",
"uses": ["Services"],
"tags": ["client care"],
"placements": [{ "documentTypeKey": "engagement-letter", "section": "terms", "order": 4 }]
},
{
"key": "sign-off",
"title": "Sign-off",
"description": "The closing and the firm's name.",
"useWhen": "Every engagement letter.",
"script": "Yours sincerely,\n\n{{ firm.name }}",
"placements": [{ "documentTypeKey": "engagement-letter", "section": "closing", "order": 1 }],
"unnumbered": true
}
],
"templates": [
{
"documentTypeKey": "engagement-letter",
"name": "Standard engagement",
"description": "With the complaints clause.",
"blockKeys": ["complaints"]
}
],
"selectionTests": [
{
"documentTypeKey": "engagement-letter",
"name": "Hourly rates",
"brief": "New client Jane Smith of 1 High Street, Bath, about a boundary dispute with her neighbour. She will be charged at our hourly rates.",
"expectedBlockKeys": ["fees-hourly"],
"forbiddenBlockKeys": ["fees-fixed"]
}
]
}
What the example shows:
- Firm name has no
documentTypeKeys, so every document type can use it, and a default with noallowUserChange, so it is fixed. If your workspace already has afirm.namefield (the example libraries bring one), the import asks you to choose: use the existing field. - Fees: a fixed fee and Fees: hourly rates are alternatives in one
variantGroup. Each hasautoand anincludeWhen, so the answer to Fee basis adds one of them, and the form asks Fixed fee only when the fixed-fee block is in the plan. - Scope of the work defines Services, which three blocks use. Complaints refers to the scope clause by its key:
{{ ref "scope" }}prints its number. - After the import, open each of the six blocks on the Blocks tab and choose Publish. The template and the selection test are ready once the blocks are published.
Build one with an AI assistant
A general-purpose AI assistant can turn a document your firm already uses into a first draft of a library file: the fields from the facts that change between documents, a block for each clause, alternatives as variant groups. You review the result in the library like any other import.
Your precedents may be confidential. Use only an assistant your organisation permits for that material: what you give an outside assistant leaves your workspace. To write or improve one clause at a time, the block editor's AI drafting runs on your workspace's own model infrastructure, and nothing is sent to a third party.
Gather what the assistant needs: the document you are starting from, the schema from this page, the block language reference, and a complete file to copy the style from, such as the example above.
Ask for
library/library.json, saying what to turn into what. For example:textTurn the attached precedent into a drafting library file, library/library.json, valid against the attached JSON Schema. The block language is described in the attached reference, and the attached example shows the style. - One document type, key "engagement-letter", with the precedent's headings as sections, in order. - A field for every fact that changes from one document to the next, keyed group.fact (client.name, matter.start_date), with the kind that fits the answer. - A block for each clause, its text in the block language, merging the fields with {{ field.key }}. Keep the precedent's wording. - Where the precedent offers alternatives, a block for each alternative in one variantGroup, with an includeWhen over the field that decides between them. - For every block, a description and a useWhen written for a colleague who has never seen the precedent, and a placement naming one of the type's sections. - Put any fact you are unsure of in square brackets in the text, and list your assumptions after the file.For a long document, ask for the fields first, then the blocks a section at a time, and put the lists together into one file.
Check the result against the schema. Many code editors check a JSON file against a schema as you type once you point them at it, and mark a misspelt property or an unknown value.
Write
manifest.json, make the zip and import it. Read the notes the import shows.On the Blocks tab, open each block. The editor lists any error in its text with a fix where the fix is mechanical; correct each block against your precedent and Publish it.
Run the selection tests once the blocks are published, then widen Who can use on the document type and its templates.
Other ways to use the file
- Start from an example. Unzip an example library, change it, zip it again and import it: a quick way to set up a library shaped like one you know.
- Copy a library to another workspace. Export the document type on its card, and import the zip in the other workspace.
- Make the same change in many blocks. Export, edit the file (rename a defined term in every block, say) and give the document type a new name, then import it, check it and archive the old type.
- Keep the source. A library file is a readable record of a library at a moment. Keep exports beside your precedents, and compare two to see what changed.
Make the zip
Zip the two items, manifest.json and the library folder, not a folder that holds them: the import looks for manifest.json at the top of the zip.
Windows: select both, right-click and choose Compress to ZIP file (on earlier versions, Send to → Compressed (zipped) folder).
macOS: select both, Control-click and choose Compress 2 Items.
PowerShell 7, in the folder that holds them:
powershellCompress-Archive -Path manifest.json, library -DestinationPath engagement-letter.zipUse PowerShell 7 rather than Windows PowerShell 5.1, which writes the folder name in a way the import can't read.
Terminal on macOS or Linux, in the same folder:
bashzip -r engagement-letter.zip manifest.json library
If the import is refused
| The message says | What to do |
|---|---|
| The file is not a Ferrith bundle — it has no manifest. | manifest.json isn't at the top of the zip. Zip the two items, not the folder that holds them. |
| The bundle has no library. | library.json isn't at library/library.json. Check the folder's name, and zip it with one of the methods above. |
| The bundle contains an entry that is not valid JSON. | A comma, quotation mark or bracket is missing or extra. Check the file against the schema. |
| This is not a library bundle. | kind isn't library. |
| This bundle was exported by a newer version of Ferrith than this workspace runs. | Set formatVersion to 1. |
| A field in the bundle has no valid key, label or kind. | Check the field's key against the shape above, and the spelling of its kind. |
| Pick one of the numbering styles. | numbering is missing or misspelt. |
| Pick a heading size from 1 to 4. | sectionHeadingLevel is outside 1 to 4. Leave it out for the default, 2. |
| The default isn't a valid value for this field, or doesn't pass the field's own rules | Correct the field's defaultValueJson, or its rules: see the default answers above. |
| The header (or footer) has an error at line… | Correct the header or footer; the rest of the message names the error. |
| That file is larger than the 32 MB library bundle limit. | Remove what the library doesn't need, or split it into two document types. |
| This workspace is limited to … templates and cannot fit the library's … | Delete templates you no longer need or ask support to raise the limit, or take some templates out of the file. |
| The field … isn't in this library. | A field group names a field that isn't in the file. |
The schema
Two JSON Schemas (draft 2020-12), one for each file. A file that passes both has the shape the import reads; the checks the schema can't express, such as a valid default answer, are in what the import checks.
manifest.json:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Ferrith drafting library file: manifest.json",
"type": "object",
"required": ["formatVersion", "kind", "name"],
"additionalProperties": false,
"properties": {
"formatVersion": { "const": 1, "description": "The file format version." },
"kind": { "const": "library", "description": "What the file holds." },
"name": { "type": "string", "minLength": 1, "description": "The library's name, shown when it is imported." },
"exportedAtUtc": { "type": "string", "format": "date-time", "description": "When the file was written, in UTC." },
"entityNames": {
"type": "array",
"items": { "type": "string" },
"description": "The names of the document type and its templates."
},
"notices": {
"type": "array",
"items": { "type": "string" },
"description": "Notes shown to the person importing the file."
}
}
}
library/library.json:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Ferrith drafting library file: library/library.json",
"description": "One document type with everything it stands on: its fields, building blocks, templates and selection tests, each referring to the others by key. A property left out takes its default: an empty list, null or false.",
"type": "object",
"required": ["documentTypes"],
"additionalProperties": false,
"properties": {
"documentTypes": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/documentType" },
"description": "The document type, as a list of one."
},
"fields": {
"type": "array",
"maxItems": 2000,
"items": { "$ref": "#/$defs/field" },
"description": "The questions the type's documents ask."
},
"blocks": {
"type": "array",
"maxItems": 2000,
"items": { "$ref": "#/$defs/block" },
"description": "The building blocks."
},
"templates": {
"type": "array",
"items": { "$ref": "#/$defs/template" },
"description": "Fixed recipes of blocks for the type."
},
"selectionTests": {
"type": "array",
"items": { "$ref": "#/$defs/selectionTest" },
"description": "Briefs that measure how well the AI picks blocks."
}
},
"$defs": {
"key": {
"type": "string",
"maxLength": 64,
"pattern": "^[a-z0-9][a-z0-9-]*$",
"description": "The key of a document type, section, field group or block: lower-case letters, digits and hyphens."
},
"fieldKey": {
"type": "string",
"maxLength": 64,
"pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*){0,5}$",
"not": {
"pattern": "^(document|page|money|words|ordinal|format_date|names|list|ref|image|anchor|page_break|string|array|math|date|object|regex|timespan|html|if|else|end|for|in|while|case|when|func|ret|with|wrap|capture|include|import|readonly|true|false|null|empty|tablerow|break|continue|this|loop)(\\.|$)"
},
"description": "A field key: lower-case words joined by dots and underscores, at most six words. The first word can't be one of the block language's own names."
},
"keyList": { "type": "array", "items": { "$ref": "#/$defs/key" }, "uniqueItems": true },
"fieldKeyList": { "type": "array", "items": { "$ref": "#/$defs/fieldKey" }, "uniqueItems": true },
"textList": { "type": "array", "items": { "type": "string" } },
"documentType": {
"type": "object",
"required": ["key", "name", "numbering"],
"additionalProperties": false,
"properties": {
"key": { "$ref": "#/$defs/key" },
"name": { "type": "string", "minLength": 1, "description": "The name members see." },
"description": { "type": ["string", "null"] },
"sections": {
"type": "array",
"items": { "$ref": "#/$defs/section" },
"description": "The headings, in order. A part comes after the schedule it belongs to."
},
"numbering": {
"enum": ["continuous", "by_section", "none"],
"description": "continuous: 1, 2, 3 through the document. by_section: 3.1, 3.2, restarting in each section. none: no clause numbers."
},
"numberSectionHeadings": { "type": "boolean", "description": "With numbering by_section, put a numbered section's number in its heading." },
"sectionHeadingLevel": {
"anyOf": [{ "type": "integer", "minimum": 1, "maximum": 4 }, { "type": "null" }],
"description": "How large the section headings print, 1 (the title's size) to 4; parts one size smaller. Null or left out: 2."
},
"listNumbering": {
"enum": ["plain", "legal", null],
"description": "plain: a block's numbered lists print as written. legal: they print as sub-paragraphs under the clause, 4.7.1 then (a) then (i)."
},
"mandatoryBlockKeys": {
"$ref": "#/$defs/keyList",
"description": "The blocks every document of the type includes."
},
"fieldGroups": {
"type": "array",
"items": { "$ref": "#/$defs/fieldGroup" },
"description": "The headings the form asks its questions under."
},
"selectionGuidance": {
"type": ["string", "null"],
"description": "Guidance for the AI when it proposes blocks for the type."
},
"titlePattern": {
"type": ["string", "null"],
"description": "The document title, with merge fields: Engagement letter for {{ client.name }}."
},
"draftBanner": { "type": "boolean", "description": "Mark the text as a draft until the document is reviewed." },
"requireReviewBeforeExport": { "type": "boolean", "description": "Hold every export until the document is reviewed." },
"hideTitle": { "type": "boolean", "description": "Leave the title line out of the document." },
"titleAlignment": { "enum": ["left", "centre", "right", null] },
"header": {
"type": ["string", "null"],
"description": "Printed at the top of every page, in the block language. page.number and page.count print the page numbers; ref can't be used."
},
"footer": {
"type": ["string", "null"],
"description": "Printed at the foot of every page, written like the header."
},
"headerOnFirstPage": { "type": "boolean", "description": "Print the header on the first page too." },
"reviewers": {
"type": ["array", "null"],
"items": { "enum": ["admins", "owner"] },
"uniqueItems": true,
"description": "Who may review a document of the type. Named people and roles can't travel in a file."
}
}
},
"section": {
"type": "object",
"required": ["key", "title"],
"additionalProperties": false,
"properties": {
"key": { "$ref": "#/$defs/key" },
"title": { "type": "string", "minLength": 1, "description": "The heading." },
"numbered": { "type": "boolean", "description": "Number the clauses in the section." },
"hideTitle": { "type": "boolean", "description": "Leave the heading out; the section's blocks supply their own." },
"kind": {
"enum": ["clause", "schedule", "part", null],
"description": "clause: a section of the body. schedule: labelled Schedule 1, 2 as it prints. part: a part of a schedule."
},
"parentKey": {
"anyOf": [{ "$ref": "#/$defs/key" }, { "type": "null" }],
"description": "For a part, the key of its schedule, listed above it."
},
"newPage": { "type": "boolean", "description": "Start the section on a new page in the PDF and Word exports." }
}
},
"fieldGroup": {
"type": "object",
"required": ["key", "title", "fieldKeys"],
"additionalProperties": false,
"properties": {
"key": { "$ref": "#/$defs/key" },
"title": { "type": "string", "minLength": 1, "description": "The heading on the form." },
"fieldKeys": {
"$ref": "#/$defs/fieldKeyList",
"description": "The questions under the heading, in order. A field can be in one group only."
}
}
},
"field": {
"type": "object",
"required": ["key", "label", "kind"],
"additionalProperties": false,
"properties": {
"key": { "$ref": "#/$defs/fieldKey" },
"label": { "type": "string", "minLength": 1, "description": "The question, as the form shows it." },
"help": { "type": ["string", "null"], "description": "A line of help under the question." },
"kind": {
"enum": ["text", "textarea", "date", "integer", "number", "money", "boolean", "choice", "choices", "person", "persons", "address", "image"],
"description": "textarea is long text, integer a whole number, boolean yes / no, choice one of a list, choices several from a list, persons people."
},
"options": {
"type": "array",
"maxItems": 200,
"uniqueItems": true,
"items": { "type": "string", "minLength": 1 },
"description": "The answers to choose from, for choice and choices."
},
"validation": { "anyOf": [{ "$ref": "#/$defs/validation" }, { "type": "null" }] },
"documentTypeKeys": {
"$ref": "#/$defs/keyList",
"description": "The document types the field belongs to. Empty: every document type in the workspace."
},
"defaultValueJson": {
"type": ["string", "null"],
"description": "A default answer, written as JSON in a string: \"\\\"Bath\\\"\" for text, \"\\\"2026-10-01\\\"\" for a date, \"12\" for a number, \"true\" for yes / no, \"[\\\"Email\\\"]\" for several from a list, \"{\\\"full_name\\\": \\\"Jane Smith\\\", \\\"address\\\": \\\"1 High Street\\\"}\" for a person. Always null for an image."
},
"allowUserChange": {
"type": "boolean",
"description": "With a default: true lets the person drafting change it; false makes it fixed."
},
"imageWidthMm": {
"type": ["integer", "null"],
"minimum": 5,
"maximum": 170,
"description": "For an image, the printed width in millimetres. Null: 40."
}
},
"allOf": [
{
"if": { "properties": { "kind": { "enum": ["choice", "choices"] } } },
"then": { "required": ["options"], "properties": { "options": { "minItems": 1 } } }
},
{
"if": { "properties": { "kind": { "const": "image" } } },
"then": { "properties": { "defaultValueJson": { "type": "null" } } }
}
]
},
"validation": {
"type": "object",
"additionalProperties": false,
"properties": {
"minLength": { "type": ["integer", "null"], "minimum": 0, "description": "Text, long text, address: the shortest answer." },
"maxLength": { "type": ["integer", "null"], "minimum": 0, "description": "Text, long text, address: the longest answer." },
"min": { "type": ["number", "null"], "description": "Whole number, number, money: the lowest answer." },
"max": { "type": ["number", "null"], "description": "Whole number, number, money: the highest answer." },
"pattern": { "type": ["string", "null"], "description": "Text, long text, address: a regular expression the answer must match." },
"minItems": { "type": ["integer", "null"], "minimum": 0, "description": "Several from a list, people: the fewest answers." },
"maxItems": { "type": ["integer", "null"], "minimum": 0, "description": "Several from a list, people: the most answers." }
}
},
"block": {
"type": "object",
"required": ["key", "title", "script"],
"additionalProperties": false,
"properties": {
"key": { "$ref": "#/$defs/key" },
"title": { "type": "string", "minLength": 1 },
"description": {
"type": ["string", "null"],
"description": "What the block says. The AI reads it, with useWhen, when it proposes blocks."
},
"useWhen": { "type": ["string", "null"], "description": "When to choose the block, and over which alternative." },
"script": {
"type": "string",
"minLength": 1,
"description": "The block's text in the block language: Markdown, with merge fields, conditions and repeats between double braces. A new line is \\n."
},
"guidance": { "type": ["string", "null"], "description": "Guidance for the person drafting." },
"includeWhen": {
"type": ["string", "null"],
"description": "The plan's test over the answers: a yes / no compares with `true` or `false`, a number with the number in backticks, text with text in single quotes, as in matter.fee_basis == 'Fixed fee'."
},
"defines": { "$ref": "#/$defs/textList", "description": "The defined terms the block defines." },
"uses": { "$ref": "#/$defs/textList", "description": "The defined terms the block uses." },
"placements": {
"type": "array",
"items": { "$ref": "#/$defs/placement" },
"description": "Where the block sits in each document type. A block needs one to be published."
},
"tags": { "$ref": "#/$defs/textList", "description": "Labels for finding the block." },
"optionalFieldKeys": {
"$ref": "#/$defs/fieldKeyList",
"description": "Fields the block merges that may be left unanswered."
},
"requiresKeys": { "$ref": "#/$defs/keyList", "description": "Blocks that must be in the same document." },
"conflictsKeys": { "$ref": "#/$defs/keyList", "description": "Blocks that can't be in the same document." },
"variantGroup": {
"type": ["string", "null"],
"description": "Blocks in the same group are alternatives; a plan takes at most one."
},
"auto": { "type": "boolean", "description": "Add the block to the plan by itself when includeWhen holds." },
"unnumbered": { "type": "boolean", "description": "Print the block without a clause number." }
}
},
"placement": {
"type": "object",
"required": ["documentTypeKey", "section", "order"],
"additionalProperties": false,
"properties": {
"documentTypeKey": { "$ref": "#/$defs/key" },
"section": {
"type": "string",
"pattern": "^$|^[a-z0-9][a-z0-9-]*$",
"description": "The key of one of the type's sections; empty when the type has none."
},
"order": { "type": "integer", "description": "The block's position in the section, lowest first." }
}
},
"template": {
"type": "object",
"required": ["documentTypeKey", "name", "blockKeys"],
"additionalProperties": false,
"properties": {
"documentTypeKey": { "$ref": "#/$defs/key" },
"name": { "type": "string", "minLength": 1 },
"description": { "type": ["string", "null"] },
"blockKeys": {
"$ref": "#/$defs/keyList",
"description": "The blocks a document starts with, beside the ones the type always includes."
},
"requireReviewBeforeExport": { "type": "boolean" }
}
},
"selectionTest": {
"type": "object",
"required": ["documentTypeKey", "name", "brief"],
"additionalProperties": false,
"properties": {
"documentTypeKey": { "$ref": "#/$defs/key" },
"name": { "type": "string", "minLength": 1 },
"brief": { "type": "string", "minLength": 1, "description": "Written the way a member would describe the document." },
"expectedBlockKeys": { "$ref": "#/$defs/keyList", "description": "Blocks a right answer must pick." },
"forbiddenBlockKeys": { "$ref": "#/$defs/keyList", "description": "Blocks a right answer must not pick." }
}
}
}
}
Related: the drafting library, where the imported document type, fields and blocks are maintained.