DEV Community

Kevin Bjorvand
Kevin Bjorvand

Posted on Fully Autonomous

Fail-closed accounting workflows: n8n, TypeScript, and a Conta pilot

A workflow can finish successfully and still have no valid accounting conclusion. That distinction drives the design of this open-source Conta intercompany balance checker.

The project combines n8n orchestration with deterministic TypeScript logic. It compares manually selected accounts for two companies and produces Norwegian HTML, CSV and JSON evidence for an accountant.

Current status: this is a pilot. Synthetic tests and local self-hosted n8n tests have passed. Validation against reports from two authorized Conta companies and execution on n8n Cloud are still outstanding. Nothing below establishes a production-ready integration or a completed reconciliation.

Separate data completeness from balance agreement

Consider two synthetic companies:

Signed NOK balance Company A Company B A + B
Opening 100,000 -85,000 15,000
Period movement 150,000 -140,000 10,000
Closing 250,000 -225,000 25,000

The closing difference is NOK 25,000, but NOK 15,000 was already present at the start of the month. Reporting the entire difference as something caused this month would be misleading.

The comparison uses signed balances:

closing residual = closing balance A + closing balance B
opening residual + movement residual = closing residual
Enter fullscreen mode Exit fullscreen mode

Taking absolute values before comparing would lose the distinction between a receivable and a payable.

The report exposes two separate decisions:

Data status Balance status Meaning
COMPLETE BALANCES_AGREE Required checks passed; residual is within tolerance.
COMPLETE DIFFERENCE Required checks passed; residual exceeds tolerance.
INCOMPLETE null There is insufficient evidence for a balance conclusion.

An absent account, failed request or malformed response must never become a zero balance. Even COMPLETE plus BALANCES_AGREE does not prove that all transactions were recorded or that an accountant approved the reconciliation.

Keep orchestration and accounting logic separate

n8n handles requests, branching, bounded retries and scheduling. TypeScript handles scope validation, response adaptation, exact arithmetic, candidate suggestions and report rendering.

The TypeScript modules are bundled into JavaScript Code nodes. The shipped workflows require no runtime npm imports or separate backend.

For each run, the request sequence is:

  1. Retrieve each company's account list and initial trial balance.
  2. Retrieve ledger details for every selected account on both sides.
  3. Retrieve both trial balances again.
  4. Check each account's opening + movement = closing, detail sum = movement, and unchanged selected totals across the two retrievals.

The final retrieval catches some changes during collection. It is not an atomic snapshot: offsetting edits that leave totals unchanged can still escape that check.

Account ownership is an explicit configuration boundary. An accountant must approve dedicated accounts for the relationship. The workflow does not infer counterparties from amounts or descriptions.

Preserve precision before calculating anything

The local n8n HTTP-node tests exposed a transport issue: text response mode parsed JSON internally and rounded a 64-bit source ID. Converting that rounded value to a string later cannot restore the missing digits.

The tested configuration therefore receives the response as a file, then decodes its raw bytes inside a Code node:

const raw = await this.helpers.getBinaryDataBuffer(0, 'raw');
const text = raw.toString('utf8');
// Pass this text to the token-preserving parser before mapping values.
Enter fullscreen mode Exit fullscreen mode

The project's parser preserves numeric JSON tokens as strings for authoritative decoding. Its implementation and limits are visible in transport.ts.

Money is converted from validated decimal strings to integer øre with BigInt; one NOK is 100 øre. Values cross JSON boundaries as decimal strings. Exponent notation and amounts with more than two fractional digits are rejected instead of silently rounded.

const closingA = 25_000_000n;   // NOK 250,000.00
const closingB = -22_500_000n;  // NOK -225,000.00
const residual = closingA + closingB;
// 2_500_000n øre = NOK 25,000.00
Enter fullscreen mode Exit fullscreen mode

The runtime also exposed a bundling constraint: the hardened n8n runner rejected export helpers using Object.defineProperty. The build now emits a plain bundled object, and a regression test exercises that restriction.

Treat failure paths as report behavior

The request loop allows at most three attempts per request. It honors Retry-After seconds and HTTP dates. An excessive wait stops the run instead of retrying earlier than the server requested. Authentication failures and redirects do not retry.

All Conta requests are GET-only and restricted to the configured Conta gateway and endpoint paths. Credentials stay in n8n credentials. This restriction does not make the API key itself read-only: Conta keys inherit their user's privileges.

An INCOMPLETE report can be the output of a successful n8n execution. Operators must inspect both the execution status and the report status. A missing report after a host or runner failure is also not a valid result.

Ledger candidates are conservative. Suggestions require an explicitly confirmed shared invoice reference, uniqueness on each side, opposite equal amounts and dates within the configured window. Local voucher IDs are not shared matching keys. Duplicates, split settlements, reversals and corrections remain review items.

What has actually been tested

As of September 17, 2026:

  • 53 deterministic accounting/workflow tests passed.
  • 17 scenarios passed through actual self-hosted n8n 2.39.6 execution on Windows with Node 24.13.1, synthetic credentials and a loopback fixture server.
  • Those scenarios covered both credential branches, multiple accounts, Retry-After seconds/date, exhausted retries, three real 30-second timeouts, 401/403 responses, malformed and empty bodies, missing accounts, inconsistent movements, changing balances, large IDs and redirects.
  • A real local Schedule Trigger generated a saved synthetic report; HTML, CSV and JSON were read back from execution storage.
  • n8n's automatic pruning removed a disposable copy of that execution and its inline reports. This did not test Cloud retention, external binary stores, backups or physical secure erasure.

The runtime record includes workflow hashes and separates execution success from report completeness.

These tests establish the behavior of the tested implementation and fixtures. They do not establish Conta's live date, omission or correction semantics. Those checks remain explicit release gates.

Reproduce the synthetic example

The MIT-licensed repository contains the modules, fixtures, tests and inactive workflow exports.

With Node 22.18 or newer, run the local deterministic checks:

git clone https://github.com/KevinBjorv/conta-intercompany-checker.git
cd conta-intercompany-checker
npm ci
npm run check
Enter fullscreen mode Exit fullscreen mode

To try the n8n demo, import workflows/conta-synthetic-demo.json, run Manual demo, and download the three files from Private reports. That workflow makes no network requests and needs no credentials.

The fixed example contains 20 lines, a NOK 25,000 closing residual, nine candidate pairs and two unresolved lines. You can inspect the Norwegian sample report or the 90-second silent Norwegian demonstration.

The separate live template stays inactive and reports INCOMPLETE until its operator has documented the required report validation. Balance agreement remains evidence for review, never automatic accountant approval.


AI disclosure: This article was written by an AI assistant from the project's source code and recorded test evidence. All company examples are synthetic. The checker itself uses deterministic accounting logic, not an LLM.

Top comments (0)