Automation System: Architecture & API Manual

Close Documentation
01 Business Overview

What Does This System Do?

When a funeral booking is placed, this system reads every file the customer uploaded, looks up any missing hymn or prayer text, applies branch-specific formatting rules, and produces a ready-to-use InDesign file, entirely automatically. No manual typing. No copy-pasting.

Reads the attachments
Opens Word documents, PDFs, and scans photos of handwritten notes to extract the full service content: names, dates, hymns, readings, tributes, and any special instructions.
Looks up missing text
If a hymn, prayer, or poem is requested by title only (e.g. "The Lord's Prayer" or "Abide With Me"), the system checks the internal content library. If a match is found, the full text is inserted automatically. If not, the title is placed in the booklet as-is for the designer to complete.
Applies branch formatting
Each arranger branch has its own formatting rules: capitalisation, spelling, layout preferences. These are applied automatically to every order from that branch without any manual intervention.
Hands off to the designer
Produces a structured file that populates an InDesign template automatically. The designer receives a filled file, sense-checks it, makes final adjustments, and approves for print.
💡
In simple terms
Order comes in → system reads everything uploaded → fills the booklet template → designer gets a pre-populated InDesign file to check and approve. The manual data entry step is eliminated entirely.

What it handles automatically

Customer providesSystem does
A Word document with service notesReads and extracts all text from the document
A photo of handwritten instructionsScans the image using OCR to read the text
Just a hymn title (e.g. "Jerusalem")Checks the internal content library; inserts full text if found, or the title only if not found
A name in lowercase (e.g. "john smith")Applies branch rules, e.g. outputs "JOHN SMITH" if capitalisation is required
No running order specifiedUses the branch's typical page layout as a guide for content distribution
A modern copyrighted songInserts a placeholder and flags it as a potential data entry error
01 Business Deep Dive

Full Business Walkthrough

Everything that happens from the moment a booking is placed to the moment a designer opens the InDesign file. This section explains every decision point, how edge cases are handled, and what happens when things don't go as expected.

The Journey of a Single Order

Every booklet order passes through the same eight stages. Here is each step in plain English with a real-world example throughout.

1
Funeral director completes the booking
A funeral director logs into the booking platform and fills in the order: the deceased's name and dates, which branch they're from, which booklet product they've ordered, and any notes. They also upload supporting files, typically a Word document or PDF with the running order, and sometimes a photo of handwritten instructions from the family.
ExampleBranch 9 places an order for Eleanor Vance. They upload instructions.docx and handwritten_note.jpg.
2
Enfocus Switch detects and forwards the job
Switch monitors the booking platform continuously. The moment a new job arrives, it packages the order data as a JSON payload and sends it as an HTTP POST request to the middleware's /process endpoint. This happens within seconds of the booking being created, with no manual triggering required.
3
Attachments are downloaded and read
The middleware downloads every file linked in the order. Word documents are parsed for their full text. PDFs have their text layer extracted. Images (photos, scans) are scanned using OCR to read any written content. The extracted text from all files is combined into a single block that goes to the AI alongside the order data. A smart filter discards noise like camera metadata, copyright notices, and logo text before it reaches the AI.
Exampleinstructions.docx → full text extracted.
handwritten_note.jpg → OCR reads "please include The Lord's Prayer on page 6".
4
Branch rules and layout model are loaded
Based on the arranger ID in the order, the middleware loads the branch-specific formatting rules: a plain English text file that the AI reads and applies directly. The global master rules (which apply to every order from every branch) are also loaded. If this branch has a consensus layout model (a statistical map of how previous orders from this branch were typically laid out), it is also loaded as a fallback guide for page distribution.
ExampleBranch 9's rules say: capitalise the deceased's name, use traditional spelling for hymn lyrics, bold all prayer titles.
5
AI fills in the booklet template
Claude receives the blank XML template, the full order data, all extracted attachment text, and all loaded rules in a single structured prompt. It populates every field: the deceased's name and dates on the cover, then distributes the service content (hymns, readings, tributes, prayers, committal) across the inner pages (Page 2 through Page 7) and the back page message. The customer's running order, if specified, is followed exactly. If not, the branch's typical layout model is used as a guide.
Example"eleanor vance" → ELEANOR VANCE (branch capitalisation rule applied).
Running order followed: Welcome → Hymn → Eulogy → Poem → Lord's Prayer → Committal.
6
Missing hymn and prayer text is looked up
If any hymn, prayer, or poem was referenced by title only, Claude queries the internal content library. If a match is found, the full text is placed automatically into the correct page field. If no match is found, only the title is inserted — the designer adds the body text. For modern copyrighted songs (post-1930 pop, rock, R&B) a clear placeholder is inserted regardless of the library, and a note is added to the job log flagging it for the designer.
Example"The Lord's Prayer" → found in library → full text placed on Page 6 in bold.
"A custom poem" → not in library → title only inserted; designer adds the body.
"Angels by Robbie Williams" → modern copyright → placeholder inserted + note added.
7
XML is validated and returned to Switch
Before the XML leaves the middleware, it is parsed by a dedicated validator. If the XML is well-formed it is returned to Switch with HTTP 200 and logged as "success". If it is malformed (unclosed tag, illegal character, encoding error) it is returned with HTTP 422 so Switch knows to route the job to a manual review path rather than passing broken data to InDesign. The exact fault location (line and column) is logged.
8
Designer receives the ready InDesign file
Switch routes the validated XML into the InDesign Server hot folder. InDesign Server ingests it and automatically maps every field into the template placeholders, producing a packaged file. The designer opens it, sense-checks the content, makes any final formatting adjustments, and approves it for print. There is no data entry left to do, only quality review.

Attachment Processing

The system handles three types of file attachment, each processed differently.

File typeHow it's readWorks well forLimitations
.docx Word document Parsed with the mammoth library. Full text extracted directly. Any standard Word document: typed running orders, service notes, tributes. Heavily formatted tables or unusual embedded content may not extract cleanly.
.pdf PDF document Text layer extracted with pdf-parse. Digitally created PDFs where text was typed directly. Scanned/image PDFs (a photo saved as PDF) return no text; the image inside would need OCR.
.jpg / .png / .webp Image Scanned locally with Tesseract.js OCR (runs on the server, no external API). Photos of printed or clearly typed text, neatly written handwritten notes. Heavily cursive or scrawled handwriting may not extract reliably. Claude Vision is available as an upgrade path for difficult handwriting.
🔍
OCR noise filtering
After OCR runs on an image, the extracted text passes through a heuristic filter before it is included in the AI prompt. The filter discards text that looks like camera EXIF data, copyright notices, file names, Lorem Ipsum placeholder text, or short random strings. Only text that looks like service content, instructions, or a running order is passed through. This prevents background noise from polluting the booklet content.

Formatting & Rules Priority

The system uses a three-tier instruction hierarchy to decide which rules take precedence when there is a conflict.

PriorityRule sourceWhat it controlsCan it be overridden?
Level 1 (Highest) Customer's running order / page sequence Which content goes on which page and in what order. No. If the customer specifies a sequence, it is followed exactly and overrides the consensus layout.
Level 2 Branch-specific formatting rules (Branch/<id>.txt) Capitalisation, spelling, bold/italic conventions, date formats, branch-specific preferences. Only by master rules where they conflict. The customer supplies the data; the branch rules govern how it is presented.
Level 3 Master rules (master/master.txt) Global standards applied to every order from every branch. No; these are the baseline for everything.
⚠️
Important distinction: sequence vs. formatting
Level 1 (customer running order) only controls which content appears on which page. It does not override formatting. For example, if the customer enters "matthew wymer" and the branch rule says capitalise the deceased's name, the output will be "MATTHEW WYMER" regardless. The customer's value is the source data; the formatting rules always govern presentation.

How to update rules

Rules are plain text files written in plain English; Claude reads and interprets them directly. To change a rule for a specific branch, edit the file at Branch/<arranger_id>.txt on the server. To change global rules, edit master/master.txt. Changes take effect immediately on the next order, with no restart required.

What Happens When Things Go Wrong

The system is designed to degrade gracefully; a single problem with one part of an order does not stop the whole job.

What goes wrongWhat the system doesWhat the designer sees
A file URL fails to download Skips that file, continues processing all other attachments. Booklet populated without that file's content. May need manual additions.
OCR finds no readable text in an image Logs a warning, continues without that image's content. Content from that image will be missing. Check the original file.
Content is referenced by title only and is not in the library The title is inserted into the field. No external search is made. Title appears in the booklet; designer adds the full text if needed.
The order has no product name Immediately exits: no AI call, no processing. Logged as "Not an OOS". Not a booklet order; skipped silently.
AI returns malformed XML Validation catches it. Returns HTTP 422 to Switch. Switch routes to manual review path. Job flagged for manual review in Switch; does not reach InDesign with broken data.
💡
Every job is logged
Every order processed, successful or not, is written to the local audit database. The log includes the exact rules that were applied, the full output XML, the processing time, and any notes generated during processing. This makes it easy to investigate any issue after the fact.
Section 2: Technical
02 Technical Overview

Technical Architecture

A Node.js/Express service running on Ubuntu, coordinating document parsing, OCR, AI generation, and XML validation in a single synchronous request–response cycle. The full pipeline completes in approximately 15–30 seconds per order.

1. Origin: Ultimate
Order Entry & Asset Upload
The order is captured in the "Ultimate" booking platform. Customers upload service details: PDFs, Word documents, text files, or photos of handwritten notes.
2. Router: Switch
Job Capture & HTTP Forward
Enfocus Switch detects the new job, packages metadata as JSON, and sends an HTTP POST to the middleware's /process endpoint.
3. Brain: Middleware
AI Processing & XML Generation
Downloads assets, extracts text (OCR + parsers), loads branch rules, queries Claude to populate the XML template, checks the content library for hymn and prayer text, validates output, logs to the audit database.
4. Router: Switch
XML Gathering & InDesign Handshake
Receives the structured XML response, downloads linked layout images, and routes files directly into the Adobe InDesign Server hot folder.
5. Render: InDesign
Template Population & Package
InDesign Server ingests the XML, maps content into template placeholders, and produces a packaged InDesign file.
6. Review: Designer
Sense-Check & Print Approval
Designer reviews the populated file, makes any final adjustments, and approves for print. Manual data entry is no longer required; only quality review remains.

Technology Stack

Node.js + Express
HTTP server, request routing, async orchestration
Core
@anthropic-ai/sdk
Claude API client with tool use and prompt caching
AI
Tesseract.js
Local OCR engine (WASM), no external API needed
Parsing
mammoth
.docx Word document text extraction
Parsing
pdf-parse
PDF text layer extraction
Parsing
fast-xml-parser
XML validation before returning output to Switch
Core
SQLite Database
Local SQLite audit database: job logs and dashboard
Database
PM2
Process manager; keeps both services alive and auto-restarts on crash
Ops
dotenv
Environment variable management (ANTHROPIC_API_KEY)
Ops

Key Design Decisions

DecisionReason
Prompt caching on system promptSystem prompt is static per branch; order data is dynamic. Cache hit on every repeat order from the same branch, roughly 10x cheaper on input tokens for the static portion.
Tool use for library lookupClaude triggers the library lookup tool only when it identifies a title with no body text; no unnecessary lookups on orders where all content is already provided.
Local OCR via Tesseract.jsZero cost, no external API calls, and customer images never leave the server. Fast enough for the expected image volume.
File-based rules (plain text)No code changes or server restarts needed to update formatting instructions. Any team member can edit a branch rule file immediately.
XML validation before returnPrevents silent failures. Malformed XML reaching InDesign Server would cause invisible errors in the final print file; catching it at this stage allows Switch to route the job to manual review instead.
Heuristic OCR classifierReplaced a Claude API call (used for classifying OCR output as relevant or noise) with a regex/word-count heuristic. Eliminates a small but unnecessary API cost on every image attachment.

Service Ports

ServicePortVisibilityNotes
Express Middleware (index.js)3020Public (via Nginx proxy)Receives HTTP POST from Enfocus Switch. All order processing happens here.
SQLite Database8105Localhost onlyBound to 127.0.0.1. Closed to external traffic for security. Admin UI accessible at /_/.

How the AI Works

Claude is the decision-making engine at the centre of every order. It receives the blank template, the full order data, all extracted attachment text, and all loaded formatting rules in a single structured API call — and returns a fully populated XML document. This section explains exactly what Claude sees, what it decides, and how it handles gaps in the supplied information.

Model & Cost

ModelUse caseApprox. cost / orderHow to activate
claude-haiku-4-5 Default. Fast, low cost, strong XML generation and instruction following. Handles the vast majority of orders with excellent results. ~4p Automatic — no parameter needed
claude-sonnet-4-6 Higher intelligence for complex orders: ambiguous or conflicting instructions, lengthy multi-section tributes, orders requiring more editorial judgement. ~20–30p Append ?model=claude-sonnet-4-6 to the Switch endpoint URL

What Claude Receives

Every API call is structured as two blocks: a system prompt (static per branch, prompt-cached) and a user message (unique per order, never cached). Together they give Claude everything it needs to complete the job in a single call with no follow-up questions.

SYS
System Prompt — Cached
Sent once, cached by Anthropic, reused on every subsequent order from the same branch. Invalidates only when a rule file is edited.
  • Rules 1–8: base persona, XML structure, priority hierarchy, library lookup behaviour, copyright safety, headers-only mode
  • Global master rules from master/master.txt
  • Branch-specific rules from Branch/<id>.txt
  • Consensus layout model, if one exists for this branch
MSG
User Message — Not Cached
Unique to this order. Contains everything specific to this particular job.
  • The blank XML template to be populated
  • Order data: deceased name, dates, arranger ID, product name
  • All extracted attachment text: Word documents, PDFs, and OCR output from images — concatenated into a single block

What Claude Does — Step by Step

1
Reads the template structure
Claude reads the blank XML template to understand which tags need filling: <NameOfDeceased>, <Story>, <Page2> through <Page7>, <BackPageMessage>, and so on. The template is self-describing — Claude infers the purpose of each tag from its name and the surrounding rules, with no separate schema explanation required.
2
Applies formatting rules to the order data
Using the loaded rules, Claude transforms the raw values from the order: capitalising names if instructed, formatting dates in the branch-preferred style (e.g. 1940-02-1212th February 1940), bolding prayer titles, and so on. The customer's running order — if present in the order data or in any attachment — determines which content appears on which page. If no running order is given, the consensus layout model is used as a structural guide.
3
Identifies titles that need full text
If Claude encounters a hymn, prayer, or poem referenced by title only — with no text provided — it triggers the web_search tool to check the internal content library. If the piece is found, the stored text is returned as a reference token. If not found, Claude is instructed to populate the field with the title only. For modern copyrighted songs, no library check is made — a placeholder is inserted directly and a note is added for the designer.
Decision examples"The Lord's Prayer" (no text) → library lookup → full text returned
"A custom poem" → not in library → title only inserted
"Angels by Robbie Williams" → modern copyright → placeholder inserted + note flagged
4
Tool call: web_search checks the content library
When Claude fires the web_search tool, the middleware intercepts the call and checks the internal Content Library for a matching record. If found, a {{LIB:N}} reference token is returned to Claude immediately — no network call, no cost, no latency. The token is expanded into the full body text after generation. If nothing matches, the middleware returns a "title only" instruction: Claude places just the title in the field and does not attempt to look up or write the full text from any other source.
Tool call flow (library hit)Claude → web_search("The Lord's Prayer lyrics")
→ Middleware checks Content Library → record found
→ Token {{LIB:1}} returned; full text injected after generation

Tool call flow (no library match)Claude → web_search("A poem we wrote ourselves")
→ Middleware checks Content Library → no match
→ "Title only" instruction returned to Claude
→ Title inserted; designer adds the body text
5
Returns the completed XML document
Once all fields are populated, Claude returns a complete well-formed XML document. The middleware strips any surrounding prose from the response, validates the XML with fast-xml-parser, and returns it to Switch. If the XML is valid, HTTP 200 is returned and the job is logged as success. If validation fails (unclosed tag, encoding error, etc.), HTTP 422 is returned with the fault location, and Switch routes the job to manual review — preventing broken data from reaching InDesign.

Why AI and Not a Rules Engine or Templates

📝
Unstructured, inconsistent input
No two customers supply information the same way. One attachment might be a neatly ordered Word document; another might be a photo of a handwritten note with arrows, corrections, and margin annotations. A deterministic rules engine cannot handle this variability. Claude reads all of these and interprets the intent behind them.
📋
Plain English formatting rules
Branch rules are written in plain English by anyone on the team — no regex, no code syntax, no formal grammar needed. A rule like "always bold prayer titles" or "use traditional Welsh spelling for hymns" requires nothing more than typing it in the branch file. It takes effect on the next order.
🔍
Contextual judgement on edge cases
When a customer writes something ambiguous, Claude applies sensible defaults drawn from the branch rules and consensus model, and surfaces the uncertainty in the notes field for the designer to review. Edge cases don't cause silent failures — they produce a best-effort output with a visible flag.
💡
Claude populates — it does not create
Claude's role is strictly population and formatting. It takes the customer's supplied content and reorganises, cleans, and formats it according to the rules. It only generates text independently in one situation: formatting dates into human-readable form (e.g. 1940-02-1212th February 1940). When a hymn, prayer, or poem is referenced by title only, the content library is checked — if found, the stored text is used; if not found, only the title is inserted. It never invents names, dates, tributes, or any memorial content.
02 Technical Deep Dive

Technical Reference

Full specifications for every component: server configuration, the complete API contract, OCR pipeline, rules engine, consensus model format, web scraper, database schema, and testing procedures.

Managing Rules & Branches

No code changes or server restarts are needed to update formatting rules or add a new branch. All rule files are loaded fresh from disk on every request.

Updating Global Formatting Rules

Edit the file at /var/www/middleware.justdigital.co.uk/master/master.txt. Changes take effect immediately on the next job. This file applies to every order regardless of branch.

Adding or Updating a Branch (Arranger)

Create or edit a plain text file at /var/www/middleware.justdigital.co.uk/Branch/<arranger_id>.txt. For example, Arranger ID 9 reads from Branch/9.txt. Write instructions in plain English; Claude reads and applies them directly. If no file exists for an arranger, the system falls back to master rules only.

Adding a Consensus Layout Model

Create a JSON file at Branch/<arranger_id>_consensus-model.json. See the Consensus Layout Model section for the full file format.

Adding a New InDesign Template

Add a row to FinalTemplate.csv with the product name, variant name, and the corresponding XML template filename. Place the XML template file in the templates/ directory. The middleware picks it up automatically on the next matching order.

Manually Correcting Output XML

If a job produces malformed or incorrect XML (e.g. a validation error, a missing section, or a copyright placeholder that needs replacing), the output can be edited directly in the dashboard without re-running the job. On any row in the job table, click ✎ Edit XML to open a full-screen editor pre-populated with the current output. Save the corrected XML and it will be persisted back to the audit record. The corrected XML can then be copied into Switch or InDesign manually.

Testing Without Sending to InDesign

Append ?mock=true to the endpoint URL in Enfocus Switch: POST /process?mock=true. The system runs through all file downloads and validation steps but skips the Claude API call and returns the unpopulated XML template. Safe to use in production; logs to the database with status success (mock) and costs nothing in API fees.

⚠️
After editing any rule file
No restart needed; files are read on every request. However, the first job after an edit will cause a one-time Anthropic prompt cache miss (the cache stored the old instructions). The updated instructions are then cached automatically for all subsequent jobs from the same branch.

Database API Security Rules

OperationRuleReason
List / View@request.auth.id != ""Dashboard requires a logged-in user to read records.
Create"" (open)Must stay open. The middleware writes job logs directly to the database without a user token (server-to-server on localhost). Locking this will silently break all job logging.
Updatenull (locked)Nothing in the system ever updates a log record after creation.
Deletenull (locked)Prevents accidental or malicious deletion of the audit trail.

Server Configuration & Routing

PORT
Port Configurations
  • Middleware Express App: Listens on port 3020, publicly proxied for Enfocus Switch connections.
  • SQLite Database: Listens on port 8105, bound locally to http://localhost:8105, closed to external traffic.
ENV
Environment Variables
The Express application requires a .env file in the root folder:
ANTHROPIC_API_KEY=your_claude_api_key

Network Traffic Flow

Network
[ Enfocus Switch Client ]
      │ (JSON Payload via HTTP POST)
      ▼
[ Ubuntu Server Firewall (Port 3020) ]
      │
      ▼
[ Express Middleware Service (index.js) ]
      │
      ├── (localhost:8105) ──► [ SQLite Database: job logs ]
      │
      ├── (Anthropic API) ──► [ Claude Haiku 4.5: XML generation ]
      │
      └── Compiled XML ──► Returned to [ Enfocus Switch Client ]

API Specification

POST
/process
Ingests the order payload, processes attachments, runs LLM population, and returns the compiled XML.

Query Parameters

ParameterTypeRequiredDescription
mockBooleanNoIf true, runs in dry-run mode. Downloads assets but skips Claude and OCR, returning the unpopulated template.
modelStringNoSpecifies the Claude model. Defaults to claude-haiku-4-5. Override: claude-sonnet-4-6.

Body Parameters (JSON)

ParameterTypeRequiredDescription
arranger_idStringNoUsed to load branch-specific rules from Branch/<arranger_id>.txt.
order_line_item_1_product_nameStringYesCombined with variant name to resolve the correct template. If missing or empty, middleware exits immediately with status "Not an OOS"; no AI call is made.
media_location_XURLNoAny key starting with media_location_ triggers download and text extraction of the linked file.

InDesign Template Mapping (FinalTemplate.csv)

CSV ColumnMatching LogicDescription
Product NameExact matchCompared against order_line_item_1_product_name.
Variant NameExact matchCompared against order_line_item_1_product_variant_name.
Template FileOutputResolves the target XML template filename (e.g. 4_page_standard.xml).
💡
Template file not found: automatic fallback
If the resolved template filename does not exist in the templates/ directory (e.g. the CSV references a file that has been renamed or not yet uploaded), the middleware automatically falls back to master.xml. The fallback is logged to the console and the _template_used field in the audit record is updated to master.xml, so the dashboard always reflects the template that was actually used.
{
  "id": "10092",
  "arranger_id": "9",
  "order_line_item_1_product_name": "Arrangement 9 Booklet - 4 Page",
  "order_line_item_1_product_variant_name": "Standard",
  "deceased_name": "Eleanor Vance",
  "date_of_birth": "1940-02-12",
  "date_of_death": "2026-05-18",
  "media_location_1": "http://assets.justdigital.co.uk/orders/10092/instructions.docx",
  "media_location_2": "http://assets.justdigital.co.uk/orders/10092/handwritten_note.jpg"
}

XML Element Schema

XML TagPurposeSample Output
<FrontCoverDesign>URL of the front cover InDesign template layout file.http://assets.../front_cover.indd
<Image1>URL of the customer-supplied photograph (href attribute; never modified by the AI).<Image1 href="http://...photo.jpg">
<NameOfDeceased>Full name, formatted per branch capitalisation rules.ELEANOR VANCE
<Story>Contains <DateOfBirth> and <DateOfDeath> for the front cover dates line.12th February 1940 – 18th May 2026
<BothInnerPagesDesign>URL of the inner pages InDesign template layout file.http://assets.../inner_pages.indd
<Page2><Page7>Sequential booklet page content. Claude distributes service text across these six pages.HYMN: Abide With Me\n[Full lyrics]
<BackPageMessage>Back cover text: refreshment details, thank-you note, charity requests.Refreshments at The Crown Hotel...

Response Format

FieldTypeDescription
successBooleantrue on success, false on error.
xmlString (XML)The fully populated, validated XML document ready for InDesign Server.
notesStringOperator notes from Claude: copyright warnings, manual-insert placeholders, or flags for unusual content. Empty string if none.
errorStringPresent only on failure. Human-readable error (e.g. Invalid XML: unclosed tag (line 12, col 3)). HTTP 422 is returned.
PLAYGROUND Interactive Request Builder

Test the middleware directly from your browser. Triggers a live request to the middleware service.

XML Validation Before Delivery
Before XML is returned to Switch, fast-xml-parser's XMLValidator parses it. Valid XML returns HTTP 200 and is logged as status: "success". Invalid XML returns HTTP 422 with the fault location (line and column) so Switch can route the job to a manual review path rather than passing broken data to InDesign.

Tools & Services Reference

Every tool in this system was chosen for a specific reason. This section explains what each one actually does in plain English — not just its name, but its role in the pipeline and why it was selected over alternatives.

Node.js — The Runtime

Node.js is a JavaScript runtime that executes outside the browser, directly on the server. It is built around a non-blocking, asynchronous I/O model: rather than waiting for one task to finish before starting the next, Node can kick off multiple operations simultaneously — downloading three attachments, waiting for the Claude API response, and writing a database record all at once — and handle each result as it arrives.

This model is a natural fit for this system. Almost everything it does involves waiting on something external: an HTTP download, an API call, a disk read. A traditional synchronous language would sit idle between each step. Node progresses on whichever task is ready next, making it highly efficient for workloads that are I/O-bound rather than CPU-bound.

Express — The Web Framework

Express is a minimal web framework for Node.js. It gives the application a structured way to define HTTP routes and handle requests and responses. In this system, Express does two things: it defines the /process route that Enfocus Switch sends orders to, and it automatically parses the incoming JSON body so the rest of the code works with a plain JavaScript object rather than a raw text stream.

Without Express, the same functionality could be built using Node's raw http module, but routing, body parsing, and error handling would all have to be written from scratch. Express provides these as ready-built middleware wired in with a few lines of code. The name "middleware" in this project is a reference to the wider system role — Express is just one of several npm packages the application uses.

@anthropic-ai/sdk — The Claude API Client

The official Anthropic SDK for Node.js. It handles everything involved in calling the Claude API: attaching the API key on every request, serialising the message payload into the exact format the API expects, automatically retrying on transient network failures, and parsing the response back into a usable JavaScript object.

The SDK also provides built-in support for two features this system relies on. Tool use is the mechanism by which Claude can signal mid-generation that it wants to trigger a web search, receive the results back, and then continue writing — rather than producing a complete response in one shot. Prompt caching allows specific parts of the message (the static system prompt) to be marked for caching on Anthropic's servers so they are not re-processed on every call, significantly reducing both cost and latency for repeat orders from the same branch.

Tesseract.js — Local OCR Engine

OCR (Optical Character Recognition) is the process of reading printed or handwritten text from an image file — turning a photo into extractable characters. Tesseract is Google's open-source OCR engine, originally written in C++. Tesseract.js is a WebAssembly (WASM) port of it: the same engine compiled to a format that runs natively inside a Node.js process with no external binary, no installed software, and no service dependency.

When a customer uploads a photo of a handwritten note or a scanned document, Tesseract processes that image locally on the server and returns the extracted text as a string. That string is then included in the AI prompt alongside text extracted from Word and PDF attachments, so Claude can work with all the content regardless of how it was originally supplied.

💡
Why run OCR locally rather than using a cloud service?
Running Tesseract locally means images never leave the server, there is no per-image API cost, and there is no external service that could impose rate limits or go offline. For printed text and neatly typed documents — which make up the majority of attachments — local accuracy is sufficient. Claude Vision is available as an upgrade path for heavily cursive or difficult handwriting.

mammoth — Word Document Parser

A .docx file is not a simple text file. It is a zip archive containing a bundle of XML files that together encode every paragraph, style, table, comment, and tracked change in the document. mammoth is a Node.js library that understands the Open XML specification Word uses. It opens the zip, navigates the XML structure, and returns clean plain text — handling edge cases like tracked changes, nested tables, and embedded objects that would cause a naive extraction to produce garbled output.

The result is a clean string that can be included directly in the Claude prompt with no further processing. mammoth also supports extracting rich HTML if formatting needs to be preserved, though this system uses plain text extraction.

pdf-parse — PDF Text Extractor

PDF files can contain text in two distinct ways. A text layer PDF is one where the content was typed digitally — in Word, a design tool, or a form — and the text is stored as actual characters inside the PDF. A scanned PDF is one where a physical page was photographed or scanned and saved as a PDF; the page is stored as an image, not as text, so there are no characters to extract.

pdf-parse reads the text layer. If the PDF has one, it extracts cleanly and quickly. If the PDF is a scanned image, pdf-parse returns an empty string — and that file would need to pass through the OCR image pipeline instead. Funeral directors typically supply digitally-created PDFs (exported from Word or booking systems), so the text layer is almost always present.

fast-xml-parser — XML Validator

fast-xml-parser is a high-performance XML parsing library for Node.js. In this system it is used exclusively for validation: before the populated XML is returned to Switch, the middleware runs it through XMLValidator.validate(). If the XML is well-formed — every tag opened and closed, no illegal characters, valid encoding — it passes through unchanged. If it is malformed for any reason (an unclosed tag, a stray ampersand, a broken UTF-8 sequence), the validator throws an error with the exact fault location (line and column number), and the middleware returns HTTP 422 to Switch instead of passing the broken data downstream.

This step exists because InDesign Server cannot gracefully handle malformed XML — it would either silently fail or produce a corrupted output file with no obvious indication of what went wrong. Catching the error at the middleware level means Switch can route the job to a manual review path and the designer is never presented with a broken file.

SQLite + PocketBase — Audit Database

SQLite is a file-based relational database: the entire database is stored in a single file on disk. There is no separate database server to install, configure, or connect to — the application reads and writes directly to the file using standard SQL. It is the most widely deployed database engine in the world (used in every Android and iOS device), and for a write-heavy audit log on a single server it is fast, reliable, and requires no maintenance.

PocketBase is an open-source backend built on top of SQLite. It adds a REST API (which the middleware uses to write job records), an admin dashboard accessible at /_/ (which is the "Database" interface visible in the docs dashboard), collection management, and simple authentication. The middleware writes to PocketBase's API on localhost; the admin UI provides a browser-based interface for viewing, filtering, and editing those records.

💡
Why not a cloud database?
For an audit trail and content library on a single-server deployment, SQLite is simpler, faster for local reads and writes, and requires no external service, credentials, or network round-trips. The entire database is a single file that can be backed up with a straightforward file copy. There is no database server to pay for, scale, or keep online.

PM2 — Process Manager

PM2 is a production process manager for Node.js. It solves a fundamental operational problem: if the Node application crashes, or the server is rebooted for maintenance, the application simply stops running. PM2 wraps each process and monitors it continuously: if it exits unexpectedly, PM2 restarts it automatically. It also registers processes to start on system boot, so the middleware and database come back online after a reboot without any manual SSH session or intervention.

In this deployment, PM2 manages two processes: middleware (the Express application) and middleware-pb (the PocketBase database server). Both appear in pm2 list, and their logs are streamed in real time via pm2 logs.

CommandWhat it does
pm2 listShow all managed processes, their status, restart count, and uptime.
pm2 logs middlewareStream live console output from the Express middleware.
pm2 logs middleware-pbStream live console output from the PocketBase database server.
pm2 restart middlewareRestart the Express app (e.g. after a code change to index.js).
pm2 stop middleware-pbStop the database process (needed before wiping pb_data).
pm2 describe middleware-pbShow the full process config including working directory and script path.

dotenv — Environment Variable Loader

dotenv is a small utility that reads a .env file from the project root at startup and loads its key-value pairs into process.env, making them available to the application throughout its lifetime. Environment variables are the standard way to store configuration that should not be hardcoded in source files — particularly secrets like API keys.

The .env file is excluded from version control via .gitignore, so the Anthropic API key never enters the repository or any commit history. On the production server, the file lives at /var/www/middleware.justdigital.co.uk/.env and contains a single line: ANTHROPIC_API_KEY=sk-ant-.... The application reads it on startup via require('dotenv').config().

Enfocus Switch — Workflow Router

Enfocus Switch is a print workflow automation platform that runs on a Windows machine and monitors configured data sources for new jobs. When a new booking is detected on the Ultimate platform, Switch executes a defined workflow: it packages the order metadata as a JSON payload and sends it as an HTTP POST to the middleware, then waits for the response XML. Once the response arrives, Switch downloads any linked layout assets and deposits the XML and image files into the Adobe InDesign Server hot folder.

Switch is the only integration point between the booking platform and the middleware. It handles both sides of the handshake: triggering the middleware when work arrives, and acting on the result when the middleware responds. The middleware itself is stateless — it processes one request and returns one response, with Switch managing the broader job lifecycle, error routing, and file delivery.

💡
What is a "hot folder"?
A hot folder is a directory on the file system that a service watches for incoming files. When Switch deposits a file into InDesign Server's hot folder, InDesign Server detects it automatically and begins processing — no manual trigger, no API call. It is the standard handoff mechanism between workflow automation tools and Adobe server applications in print production environments.

Adobe InDesign Server — Template Renderer

Adobe InDesign Server is a headless (no user interface) version of Adobe InDesign that runs as a background service and processes files programmatically. It watches a hot folder for incoming XML files. When Switch deposits the validated XML into that folder, InDesign Server detects it, opens the corresponding .indd template, and maps each XML tag value into the matching placeholder in the template — populating every text frame automatically. It then packages the completed InDesign file and saves it to a location the design team can access.

The link between the XML tags the middleware produces (e.g. <NameOfDeceased>, <Page3>) and the InDesign template placeholders is defined inside the template itself using InDesign's data merge or tagged text system. The middleware's responsibility ends at valid XML; InDesign Server handles all visual layout, typesetting, and file packaging from that point forward.

Extraction & OCR Engine

All attachment processing runs locally on the host server, with no external billing, maximum privacy, and client files never leaving the machine.

DOCX
Microsoft Word Parser
Uses the mammoth package to parse .docx binaries and extract clean plain text. Handles standard Word formatting reliably.
PDF
PDF Text Extractor
Uses pdf-parse to extract the text layer from digital PDFs. Note: scanned/image PDFs contain no text layer and will not extract; those would need OCR via the image pipeline.
OCR
Tesseract.js Engine
Runs a local WASM build of Tesseract OCR to scan images (.jpg, .png, .webp) for text. Zero cost, no external API calls, and images never leave the server.
🔍
OCR Quality Filter: Heuristic Classifier
After Tesseract extracts text from an image, the result passes through a heuristic filter before being included in the AI prompt. This replaced a previous Claude API call that was used for the same classification purpose, eliminating that cost entirely.

The heuristic checks: minimum word count (>6 words), minimum alpha-character ratio (>40%), absence of EXIF/metadata patterns (ISO, aperture, GPS coordinates, etc.), absence of Lorem Ipsum text, and presence of positive signals (service keywords like "hymn", "prayer", "committal"; instruction keywords like "please", "include", "page"; multi-line structured content).
  • Retained: Running orders, song/hymn titles, poems, tributes, layout instructions.
  • Discarded: Camera metadata, copyright notices, logo labels, gibberish noise, short random strings.
💡
Claude Vision: Upgrade Path for Difficult Handwriting
If the workflow begins receiving heavily cursive or complex handwritten notes that Tesseract struggles with, an upgrade path is available:
  • Modify performOcrOnImage in index.js to encode the image as Base64 and send it directly to the Anthropic Messages API with a vision-capable model.
  • Claude Vision handles cursive, corrections, arrows, and margin notes with significantly higher accuracy than Tesseract.
  • Trade-off: adds API cost (~1–3p per image) and transmits customer images to Anthropic's servers.

Tesseract vs. Claude Vision Comparison

FeatureLocal Tesseract.js (Current)Claude Vision (Upgrade Option)
CostFree ($0)~1–3p per image
Handwriting accuracyPoor for cursive / messy writingExceptional; handles cursive, arrows, margin notes
Typed text accuracyGood for standard printed fontsExcellent
SpeedFast, immediate local executionSlower; network round-trip with image upload
Data privacyMaximum; images never leave serverStandard; images sent to Anthropic's API

Dynamic Rules Engine

A file-based rules system allows formatting instructions to be updated without code changes or server restarts. All files are read from disk on every request.

M
Master Rules
Located at /master/master.txt. Contains global styling rules applied to every order from every branch: spelling preferences, capitalisation standards, typography guidelines.
B
Branch-Specific Rules
Located at /Branch/<arranger_id>.txt. Contains arranger-specific formatting preferences, local layout guidelines, and branch-specific conventions.

System Prompt Concatenation Structure

Prompt Assembly
[SYSTEM PROMPT]: cached per arranger; cache hit on every repeat order from same branch
──────────────────────────────────────────────────────────────────────
Rules 1–8: Base persona, XML structure, priority hierarchy,    (inline in index.js)
           library lookup behaviour, copyright safety,
           headers-only mode
9.  Global style rules                      (master/master.txt)
10. Branch-specific rules                   (Branch/<arranger_id>.txt)
11. Consensus layout model                  (Branch/<arranger_id>_consensus-model.json)
──────────────────────────────────────────────────────────────────────

[USER MESSAGE]: dynamic per order; never cached
──────────────────────────────────────────────────────────────────────
XML Template:  contents of the template file resolved from FinalTemplate.csv
Order Data:    {deceased_name}, {dates}, {arranger_id}, etc. (compact JSON)
Attachments:   extracted text from all downloaded DOCX / PDF / OCR files
──────────────────────────────────────────────────────────────────────
📋
Rule 8: Headers / Section Titles Only Mode
If the order data or instructions explicitly state that only section headers or titles are being supplied (e.g. "headers only", "use the section headings as given"), Claude populates each XML tag with the supplied heading text only, without performing any library lookup. This mode is never assumed; it must be explicitly requested. When in doubt, normal behaviour applies: titles are checked against the content library and the full text is inserted if found, or the title only if not.
Anthropic Prompt Caching
The entire system prompt block is marked with cache_control: { type: "ephemeral" }. Since the system prompt content is static per branch (same master rules + same branch rules + same consensus model), every repeat order from the same branch hits the cache.
  • Cache read cost: $0.08/M tokens, approximately 10x cheaper than normal input.
  • Cache threshold: 2,048 tokens minimum for Haiku 4.5; 1,024 for Sonnet 4.6.
  • Cache invalidation: Editing any rule file updates the system prompt prefix and causes a single cache miss on the next job, then re-caches automatically.

Consensus Layout Model

An optional JSON file per branch derived from historical order data. It describes the typical page-by-page structure for that arranger's booklets, giving Claude a statistically-informed fallback layout when the customer hasn't specified a running order.

FILE
File Naming
Stored in /Branch/ using the pattern:
/Branch/<arranger_id>_consensus-model.json

If the file does not exist for a given arranger, the middleware silently skips it.

JSON
File Structure
Contains a pageStats array. Each entry:
  • page: Page number (2-8). Page 8 maps to <BackPageMessage>.
  • elements: Typical content types (e.g. Hymn, Eulogy, Exit Music).
  • metrics.avgWords: Historical average word count.
  • stability: Consistency across orders (0-100%).
  • confidence: Model confidence (0-100%).
⚖️
Priority rule: customer sequence overrides consensus layout
The consensus model is a fallback only. If the customer has specified a running order in their payload or attached documents, the consensus model is completely ignored and the customer's sequence is followed exactly. The model only influences page distribution when no customer sequence is provided.

How It Is Passed to Claude

Auto-generated system prompt addition
Branch Consensus Page Layout Model (Arranger ID 9):
Use as a structural layout guide ONLY if the customer has not specified their own running order.

Typical layout for this branch:
  - Page 2 (stability: 100%, confidence: 100%): typical → Order Of Service, Entrance Music, Words Of Welcome | ~64 words
  - Page 3 (stability: 100%, confidence: 100%): typical → Order Of Service, Hymn, Entry Music | ~98 words
  - Page 4 (stability:  96%, confidence: 100%): typical → Eulogy, Poem, Tribute | ~106 words
  - Page 5 (stability: 100%, confidence:  96%): typical → Poem, Hymn, Eulogy | ~100 words
  - Page 6 (stability:  91%, confidence:  98%): typical → Poem, Exit Music, The Lord's Prayer | ~93 words
  - Page 7 (stability:  89%, confidence: 100%): typical → Committal, Exit Music, Closing Words | ~53 words
  - Back Page (stability: 93%, confidence: 90%): typical → Refreshments, Thank You, Charity | ~97 words

Content Library

An internal database of hymns, prayers, psalms, poems, and readings stored in the local SQLite database. When Claude triggers the web_search tool for an unknown title, the middleware checks this library. If a match is found, a {{LIB:N}} reference token is returned to Claude and the full body text is injected into the XML after generation — no network call, no cost, no latency. If nothing matches, Claude is instructed to use the title only; no external search is attempted.

How the Lookup Works

The middleware strips filler words from the AI's search query (e.g. "lyrics", "full text", "hymn", "prayer") to extract a clean title, then scores every record in the library using a Sørensen–Dice similarity match against the title and also_known_as fields (minimum 0.6 relevance score). If multiple variants of the same piece score equally, the middleware checks whether the AI's query mentions a variant name (e.g. "Catholic", "Welsh", "Traditional Anglican"); if so, that variant is returned. Otherwise the record marked is_default is used, falling back to the first result. When a match is found, a short {{LIB:N}} token is registered and returned to Claude; the body text is stored server-side and substituted into the final XML after Claude's response. If nothing matches, the middleware returns a "title only" instruction and no further lookup is made.

Console log examples
# Library hit — single version, no web request made
[Content Library] Hit: "The Lord's Prayer"

# Library hit — specific variant matched from query
[Content Library] Hit: "The Lord's Prayer" — Traditional Anglican

# Library hit — no variant specified, default returned
[Content Library] Hit: "The Lord's Prayer" — Catholic  (is_default)

# Miss — title-only instruction returned to Claude
[Content Library] No hit for "Candle in the Wind lyrics", returning title-only instruction

Database Schema

FieldTypeRequiredDescription
titleRich TextYesPrimary name of the piece. Supports italic/bold for stylised titles. Used as the main search key. E.g. The Lord's Prayer, Abide With Me.
typeSelectNoCategory, chosen from a fixed list: hymn, prayer, psalm, poem, reading. Returned alongside the content as context for Claude.
bodyRich TextYesThe full text, lyrics, or content. Supports bold and italic formatting which is preserved in the XML output for InDesign.
authorRich TextNoComposer, poet, or author if known. Supports italic for traditional attribution formatting. Included in the response prefix sent to Claude.
variantTextNoVersion label for pieces that exist in multiple forms. E.g. Traditional Anglican, Catholic, Modern Ecumenical, Scottish Reformed, Welsh, Scottish Gaelic, Irish. When the AI's query mentions this label, this record is selected over other variants.
is_defaultBoolNoWhen multiple variants exist for the same title, mark one record as the default. This record is returned when the order does not specify a particular version. Only one record per title should have this set to true.
also_known_asTextNoComma-separated alternative titles or common variations. E.g. Our Father, Our Father Who Art in Heaven. For native language variants, include the title in that language here so it is matched by language-specific searches.

Storing Multiple Versions of the Same Piece

Create a separate record for each version. Give each the same title, set the variant field to describe the version, and mark one record is_default to act as the fallback when no version is specified. For native-language versions (e.g. Welsh, Scottish Gaelic, Irish), add the native-language title in also_known_as so it is found by language-specific searches. A branch rule for a Welsh arranger can explicitly instruct Claude to request the Welsh version.

titlevariantis_defaultalso_known_as
The Lord's PrayerTraditional AnglicantrueOur Father, Our Father Who Art in Heaven
The Lord's PrayerCatholicOur Father (Catholic)
The Lord's PrayerModern EcumenicalOur Father (Contemporary)
The Lord's PrayerScottish ReformedOur Father (Debts)
The Lord's PrayerWelshGweddi'r Arglwydd

Adding Content

Records are managed directly in the Database Admin UI at http://localhost:8105/_/. Navigate to the content_library collection and create a new record. There is no restart required; the library is queried live on every request.

SQLite Audit Trails

Every order processed is logged to the local SQLite database, providing a complete audit trail of which rules were applied, the full output XML, processing time, and any notes generated.

FieldTypeDescription
idText (Auto)15-character unique record ID.
input_dataJSONRaw order payload from Switch plus extracted text fragments and metadata keys (_consensus_model_used, _master_instructions_used, etc.).
output_xmlTextThe finalized, populated XML returned to Switch.
statusTextsuccess / success (mock) / Not an OOS / error: invalid xml / error: <message>
duration_msNumberTotal processing time in milliseconds from ingestion to XML return.
master_instructions_usedTextExact contents of master/master.txt applied to this request.
branch_instructions_usedTextExact contents of the branch file applied to this request.
notesTextClaude-generated operator notes: copyright placeholders, manual-insert warnings, flags for unusual content.

Testing & Diagnostics

CLI
Useful Process & Log Commands
Shell
# Check all running PM2 processes
pm2 list

# View real-time logs for Node server and database
pm2 logs middleware
pm2 logs middleware-pb

# View database process state and directory
pm2 describe middleware-pb

# Wipe and recreate a fresh database instance
pm2 stop middleware-pb
rm -rf /var/www/middleware.justdigital.co.uk/pocketbase/pb_data
pm2 start middleware-pb
💡
System Resilience & Fallback Behaviours
  • Asset download failure: Skips that file, continues processing remaining attachments.
  • OCR returns blank text: Logs a warning, continues without that image's content.
  • Content library miss: Claude is instructed to use the title only; no external request is made and the field is populated with the title rather than left empty.
  • No product name in payload: Early exit; logged as "Not an OOS", no AI call made.
  • Claude returns malformed XML: Caught by validator; HTTP 422 returned, Switch routes to manual review.