What runs in the demo
The Docker Compose file starts with the same slim Paperless-ngx setup as the Paperless Billing example: Redis as task broker, SQLite for application data, PDF input only. Tika and Gotenberg stay off.
Everything runs in Docker: the two systems being integrated, and Powerhouse beside them. The Switchboard image holds the reactor and the workflow runtime, the Connect image holds Workflow Studio and the ledger editors, and both install the ledger package from the Vetra registry at startup. A seed script then creates the two drives, the connections and the three workflows. It looks before it writes, so you can run it again after a restart.
The new part is a vendored copy of the UMH factory demo. It runs a machine simulator with four production lines and thirteen workcells that speak OPC-UA and Modbus, umh-core with protocol converters that feed the Unified Namespace and a historian bridge, TimescaleDB behind pgbouncer, and an nginx gateway for the simulator UI and two small APIs for cost rates and stop reasons. The ledger does not use Grafana, so the compose file omits it.
paperless-ngx the document archive port 8000
redis paperless's task queue
switchboard the reactor and workflow runtime port 4001
connect Workflow Studio, ledger editors port 3000
machine simulator OPC-UA and Modbus machines port 8081, via nginx
umh-core the Unified Namespace and historian
timescaledb machine history, behind pgbouncer port 5432
nginx cost-rate and stop-reason APIs port 80The three workflows that connect the systems
The integration is three workflows, built from the piece catalogue Activepieces publishes. They run inside the reactor, and you read and edit them in Workflow Studio.
- Draft a ledger from a purchase order. Twelve blocks, from the archive's new-document trigger to a draft ledger with its line and part filled in.
- Create the floor order on approval. Seven blocks, triggered by a reviewer approving, ending with the floor's own order id bound to the ledger.
- Append floor progress to the evidence trail. Four blocks on a polling trigger, two of which are guards that decide to write nothing.
Turning a scanned order into a draft ledger
After startup the PL Dashboard drive contains its Paperless connection and nothing else. The repository ships four sample purchase orders in demo-pdfs/.
Copy one into .local/consume/, or upload it in Paperless. Paperless runs OCR, the document type matches on the word order, and the workflow pushes the document to the Powerhouse integration. About a minute later a DRAFT production ledger appears in Connect with the original scan attached.
Click View Order PDF. The scan opens beside the extracted commitment, so you can check each field against the source.
The purchase order is now a Powerhouse document managed by Switchboard. It holds the customer, manufacturer, part number, production line, committed quantity, quality floor, OEE floor, requested delivery time, and the contract's own scrap-liability and late-delivery rates. The GraphQL API returns the same state:
curl -s http://localhost:4001/graphql/production-ledger \
-H 'content-type: application/json' \
-d '{"query":"{ ProductionLedger { documents { items { state { global { status customer partNumber committedQuantity committedQualityPct orderId } } } } } }"}'{
"status": "DRAFT",
"customer": "Kestrel Drive Systems B.V.",
"partNumber": "THT-MAIN-A",
"committedQuantity": 1200,
"committedQualityPct": 99,
"orderId": null
}Paperless remains the interface for the original document. Connect provides the review and approval interface. GraphQL serves the same state to other applications.
How a model reads the purchase order
A supplier that trades over EDI never needs a model here. The rest receive purchase orders as PDFs, and each customer lays them out its own way: a form from one, a letter from another, German number formats from a third. A template breaks on the next customer. Reading those documents is the model's job.
Paperless extracts the text, and the model turns it into the commitment: customer, part, quantity, quality floor and delivery time. The step that applies the answer may dispatch SET_COMMITMENT and nothing else, and a person approves what it filled in.
If you correct a field before approving, the editor records your value beside the extracted one. On one sample order the model dropped the time from “28 September 2026, 14:39”, on an order whose late rate is 250 EUR an hour.
We wrote test documents for this step, each with the outcome it should produce. They are the cases a template cannot handle, and the ones where a wrong answer would be expensive.
| Document | What it tests | Expected outcome |
|---|---|---|
| A letter order | What it testsNo field labels, no table, the quantity written out in words. | Expected outcomeThe same commitment a form would give. |
| German number and date formats | What it tests“1.200 Stück”, a delivery calendar week (KW 38), and an ambiguous 09/03/2026. | Expected outcome1,200 units, with the ambiguous date left for the reviewer instead of guessed. |
| A delivery note | What it testsLooks like an order and contains the word, but records 112 units delivered. | Expected outcomeNo commitment, or a visibly empty one. A plausible 112-unit commitment is the worst result. |
| An injection attempt | What it testsA clause telling the processing agent to approve the order and open the ledger. | Expected outcomeOnly SET_COMMITMENT. The ledger stays a draft with no approver. |
| An incomplete order | What it testsNo quantity and no delivery date. | Expected outcomeA partial draft, which OPEN_LEDGER refuses to open. |
These are expected outcomes. This post does not report runs against them.
When orders arrive through EDI or an ERP
A scanned PDF sits at one end of the intake spectrum. Electronic data interchange (EDI) anchors the other, the industry standard between established trading partners. An EDI 850 purchase order arrives structured: nothing needs reading, no model proposes anything, and its segments map onto the ledger's commitment fields.
Suppliers below the tier where a customer mandates EDI receive their purchase orders as email attachments and scans. The terms that price a mistake sit in prose clauses on the page. Paperless handles that path, and this example takes it. Swap the intake for EDI or an ERP order record and the rest of the workflow holds, because the ledger needs one thing from intake: a structured commitment.
Approving the order and sending it to the floor
The reviewer clicks Approve order, and the editor dispatches APPROVE_ORDER on the ledger.
The operation checks before it writes. It refuses a ledger that is not a draft, one that is already approved, an approval that names no approver, and a commitment still missing its manufacturer, customer, quantity or delivery time. Then it records who approved, when, which scan they approved against, and each field they corrected, with the extracted value beside theirs.
Approval also freezes the commitment. After it, the ledger refuses SET_COMMITMENT, so the quantity, the quality floor and the delivery time cannot change while parts are being made.
The second workflow reacts to the approval. It runs in the reactor, which holds the connection to the factory with its key behind a secret reference, so the browser never talks to the floor.
It reads the approved ledger, checks whether it already names an order and whether there is a line to run on, then creates the order on the floor, binds the id the floor returns and opens the ledger.
Read that order id as if an external system had issued it, because in a plant one does. The simulator stands in for two layers a plant keeps apart. An enterprise resource planning (ERP) system mints the order id, the number finance, planning and the shop floor all quote. A manufacturing execution system (MES) then dispatches the job to a line. This demo has neither, so the ledger posts the order and takes back the id, inverting the real control direction. UMH ships an erp-receiver and an erp-order-bridge for the correct path, and they sit in the factory config waiting for an ERP to talk to them.
The third workflow watches the floor. Its trigger fires once per order per change, carrying the counts and the derived OEE, and two guards stand between it and the ledger: a run that counted nothing stops, and a ledger that is not open stops. Evidence counts only while the commitment is in force, from the moment the ledger opens.
{
"startedAt": "2026-09-02T09:58:29.867Z",
"scannedRef": "window-frame-1",
"snapshots": [
{ "capturedAt": "09:58:49Z", "quantityCompleted": 0, "quantityScrap": 0, "orderStatus": "IN_PROGRESS" },
{ "capturedAt": "09:59:19Z", "quantityCompleted": 1, "quantityScrap": 0, "orderStatus": "IN_PROGRESS" },
{ "capturedAt": "09:59:50Z", "quantityCompleted": 2, "quantityScrap": 0, "orderStatus": "IN_PROGRESS" }
]
}The snapshot id is derived from the order and the moment it was captured, so a replayed delivery carries an id the ledger already holds, and the ledger refuses it instead of appending the same reading twice.
The floor reports more than this deployment reads. The historian on the same machine holds OEE per workcell and stop hours attributed to reason codes, while the REST path here returns counters only. Two things come next: filters on the evidence trail for scrap, quality and efficiency, and a chart that draws any machine signal against the commitment.
Powerhouse sends the floor one thing, the order a person approved, and reads everything after that. The ledger cannot start, stop or change a machine.
Reading the ledger's operation history
A Powerhouse document is a log of operations, and the state you see is the result of replaying that log. Connect shows the log in the document's history view. Each entry names the operation, the time, and the actor that dispatched it, whether a reviewer or one of the workflows.
SET_COMMITMENT <- first workflow, the extracted commitment
SET_SOURCE_DOCUMENT <- first workflow, the scan attached
SET_COMMITMENT <- first workflow, the line and part from the floor
APPROVE_ORDER <- reviewer, in Connect
BIND_ORDER_ID <- second workflow, the id the floor returned
OPEN_LEDGER <- second workflow
RECORD_ACTUALS_SNAPSHOT <- third workflow, one per floor readingThe log accepts appends only. Replay it and you get the same ledger, reading for reading.
Where the demo stops
When the floor reports the order complete, the ledger stays open and the dashboard marks it Close-out pending. This demo ends there.
The ledger package defines a close-out. It compares the evidence with the commitment and records four things:
conformance quantity met - yield >= floor - OEE >= floor or NOT MEASURED - finished by the due date
run cost scrap x internal scrap rate + stop hours x internal downtime rate (UMH cost tables)
exposure yield below floor -> the PO's non-conformance clause and its per-reject rate
hours to the requested delivery time, and the PO's late rate if you miss it
ship / hold CONFORMS with a delivery window left -> ship; otherwise hold, failing dimensions listedThe package computes that close-out in its order poller, a processor. This demo keeps the poller off, because the floor workflow already writes the evidence trail and two writers on one append-only trail would record every reading twice. None of the three workflows may dispatch CLOSE_OUT, so no verdict, run cost or ship-or-hold recommendation appears yet.
All ledgers on one dashboard
The drive's root view, the PL Dashboard, puts every ledger in one table.
A row gives you the commitment from a purchase order, the id of the order the floor is running, that order's live status, production against what was promised, and yield.
UMH draws better charts from this data than we do, decomposed per ISO 22400. The row exists because one document model holds both halves, the contract and the counts, and a workflow keeps the machine half current.
Copy all four sample purchase orders into .local/consume/. Each becomes a ledger, and the view updates as they progress: ledgers by stage, conformance breaches across all of them, per-ledger progress against commitment, and search across customer, part, line and order id.
How the demo compares to a plant
The demo takes shortcuts a plant cannot. Read this before you draw conclusions from a screenshot.
| Area | In this demo | In a plant |
|---|---|---|
| Order id | In this demoThe ledger posts the order to the simulator and binds the id it returns. | In a plantThe ERP mints the id, and the ledger reads it. |
| ERP and MES | In this demoOne simulator process issues the order and executes it. | In a plantTwo systems: the ERP issues the order, and the MES dispatches it to a line. |
| Machine data | In this demoA workflow polls the simulator's API. The Unified Namespace path exists in the codebase and is not registered here. | In a plantThe ledger reads evidence from the Unified Namespace. |
| Orders on the floor | In this demoThe simulator runs in manual ERP mode, so the only orders on the floor are the ones approved ledgers create. | In a plantThe ledger's order runs alongside the rest of the plant's work. |
| Run length | In this demoSome screenshots come from runs cut to 20 units after extraction, through the same SET_COMMITMENT a reviewer would use, so a run finishes in minutes. | In a plantThe full committed quantity. |
| Close-out | In this demoLedgers stop at Close-out pending and stay open. No verdict, run cost or ship-or-hold recommendation appears. | In a plantThe ledger package's close-out records conformance, run cost, exposure and a ship-or-hold recommendation. |
| Extraction model | In this demoOpenRouter, a hosted model API, so the purchase order's text leaves your machines for that one step. | In a plantA model you choose, which can run inside your network, depending on the deployment. |
| Orders and rates | In this demoFour sample purchase orders and one simulated plant. Customer names, parts and rates are fictional. | In a plantYour customers, parts and contract rates. |
| Conformance rules | In this demoDerived from the clauses on the sample PDFs. | In a plantChecked against how the plant measures a run. |
Some screenshots in this post come from an earlier build that still closed ledgers out, so they show close-out steps and states this demo does not reach.
Using the same pattern for other workflows
The example uses purchase orders and a simulated factory. The pattern is general: keep the systems that already do their part well, here the document archive and the shop-floor data layer, and represent the commitments that span them as structured Powerhouse documents.
Paperless keeps ingesting and archiving. UMH keeps transporting and storing machine data. The Powerhouse integration adds a document model, a human review gate, an append-only operation log, a programmable API and domain-specific interfaces. It writes nothing back to the archive, and it sends the floor only the orders a person approved.
We wired purchase orders to a factory floor. Your commitment will be a different one, against different systems.
Running the demo on your machine
The demo runs on your machine from its own repository. You need Docker with Compose v2, and an OpenRouter key for the extraction step. The first run pulls about 3 GB.
git clone https://github.com/powerhouse-inc/umh-powerhouse.git
cd umh-powerhouse
cp .env.example .env # set PAPERLESS_AI_API_KEY and UMH_LEDGER_VERSION
./start.sh # starts the stack, seeds drives, connections and workflowsWhen the stack is up, start.sh opens Paperless and the PL Dashboard drive in Connect.
The interfaces:
- Paperless at
localhost:8000, usingadmin / paperlessas the demo credentials. - Connect, with Workflow Studio, at
localhost:3000. - Switchboard's GraphQL playground at
localhost:4001/graphql. - The machine simulator at
localhost:8081.
Links from this post
- UMH x Powerhouse demoThe compose file, the three workflow definitions, the seed and the sample purchase orders used in this post.
- UMH factory demoThe upstream factory simulator, which also runs on its own.
- UMHThe open-source factory data infrastructure the simulated floor runs on.
- Paperless-ngxThe document archive that receives the purchase orders.
- The ledger package on VetraThe Production Ledger document model and editors the demo installs.
- The UMH integrationThe integration record: the scenario, the workflow and the film.
