Powerhouse
BlogIntegrations

Following a purchase order from a scanned PDF to the UMH factory floor

A purchase order arrives as a scanned PDF in Paperless-ngx, and the factory floor reports its counts through UMH.

This walkthrough follows one order from the scan to the floor, with a Powerhouse ledger holding the contract terms and the counts side by side.

Powerhouse12 min read
Paperless holds the scans. Connect holds the ledgers they became.

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.

What runs wheretext
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 80

The 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.
The first workflow in Workflow Studio: its twelve steps, and the two runs it has made.

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.

A ledger beside the scan it came from, so each extracted field can be checked against the source. This capture comes from an earlier build that still closed ledgers out.

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:

bash
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 } } } } } }"}'
json
{
  "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.

The model's answers, as the reviewer sees them. Nothing reaches the floor until someone clicks Approve order.

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.

DocumentWhat it testsExpected outcome
A letter orderWhat it testsNo field labels, no table, the quantity written out in words.Expected outcomeThe same commitment a form would give.
German number and date formatsWhat 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 noteWhat 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 attemptWhat 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 orderWhat 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.

The second workflow, seven steps. If there is no line to run on, or the floor does not create the order, an error step records the reason on the ledger.

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.

The result: the ledger names who approved and when, and the Order ID field holds the id the floor returned.

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 simulator's Orders view. Approving a ledger adds a row like the highlighted one.

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.

json
{
  "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" }
  ]
}
Four blocks, two of them guards. A run that stops at a guard succeeded and decided to write nothing.

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 evidence trail fills as the run proceeds: committed against good and scrap, with every snapshot kept.

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.

The operations on one ledger, in ordertext
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 reading
One row per operation, in the order they were dispatched.

The 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:

text
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 listed

The 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.

The dashboard over four consumed purchase orders, each a ledger at its own stage.

How the demo compares to a plant

The demo takes shortcuts a plant cannot. Read this before you draw conclusions from a screenshot.

AreaIn this demoIn a plant
Order idIn 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 MESIn 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 dataIn 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 floorIn 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 lengthIn 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-outIn 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 modelIn 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 ratesIn 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 rulesIn 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.

bash
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 workflows

When the stack is up, start.sh opens Paperless and the PL Dashboard drive in Connect.

The interfaces:

  • Paperless at localhost:8000, using admin / paperless as 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

Start with a workflow assessment.

Find out where Powerhouse can improve operational efficiency. We map the first workflow before a build starts.

All posts