Skip to content

Invoice and expense processing

The problem

Every month the same file lands: an export from the billing system, a folder of supplier PDFs, or a shared inbox full of expense claims. Someone opens a spreadsheet and starts reconciling.

What it costs today:

  • Two days a month of an analyst who was hired to analyse, not to retype.
  • Silent errors. A mistyped amount in row 340 does not announce itself; it shows up a quarter later as a variance nobody can explain.
  • No breakdown. Asking "what did we spend with this supplier this year" means rebuilding the analysis from scratch, because the last three months of work exist only as finished spreadsheets.

What good looks like

The export goes in, a validated file comes out, and every line item leaves a countable trace behind — so the breakdown by supplier, cost centre or month is a query, not a project.

What you build

Task  (the export, in the detail or as an attachment)


 [Parse Invoices]  Node.js → validate, normalise, fall back to a sample
      │             _context: { invoices: [...], totalAmount }

 [Counter]  define-variable  processed = 0


 [For Each Invoice]  ─┬─▶ [Increment Counter]
      │               └─▶ [Record Event]  invoice_processed

 [Build CSV]  Node.js


 [Publish CSV]


 [Finish]  invoiceCount, totalAmount, processed, file

Pieces used: a Task trigger, Execute Node.js, Define Variable, an Each loop, Increment Variable, Track Event, Publish File.

Implementation

1. Take the input the way finance actually sends it

Give the flow a Task trigger. Finance can then paste a JSON array into the task detail, or attach the export as a file — both reach the flow, as detail and attachments.

2. Parse and validate before you loop

Add an Execute Node.js step. This is where the payload becomes trustworthy:

javascript
module.exports = async function (context) {
  let invoices = [];
  try {
    const parsed = JSON.parse(String(context.detail || '').trim());
    if (Array.isArray(parsed)) invoices = parsed;
  } catch (e) {}

  // A demo-safe fallback so the flow is testable without real data.
  if (invoices.length === 0) {
    invoices = [
      { id: 'INV-1001', customer: 'Acme',    amount: 1250.5 },
      { id: 'INV-1002', customer: 'Globex',  amount: 890 },
      { id: 'INV-1003', customer: 'Initech', amount: 4310.75 }
    ];
  }

  const total = invoices.reduce((s, i) => s + Number(i.amount || 0), 0);

  return {
    count: invoices.length,
    total,
    _context: { invoices, totalAmount: total }
  };
};

Two conventions matter here. Anything you return under _context becomes a top-level run variable — that is how invoices and totalAmount become available to later steps by bare name. And validating here, rather than trusting the loop, means a malformed row fails in one obvious place instead of halfway through 400 iterations.

3. Create the accumulator before the loop

Add a Define Variable step: processed, type number, value 0.

This looks like ceremony until you hit the rule it exists for: inside a loop, a body step's own output holds only the last iteration. After the loop, inc.value is the value from the final item, not the total. Anything you want to survive the loop must be a variable created before it and grown inside it.

4. Loop over the line items

Add an Each step. Its array variable is a bare path, not a template:

invoices

Not {{invoices}}. If the path does not resolve to an array the step quietly does nothing and reports an error in its result, rather than failing the run — which is exactly the kind of silent no-op you want to know about.

Inside the loop body you get item and index. Add two steps:

  1. Increment Variableprocessed, by 1.
  2. Track Event — this is the audit trail:
FieldValue
Event nameinvoice_processed
Dimension customer{{loop.item.customer}}
Measure amount{{loop.item.amount}}

One event per line item is what turns this flow from "a script that made a CSV" into a queryable spend history.

5. Build and publish the output

A second Node.js step assembles the CSV and returns it as base64 in _context; Publish File attaches it to the run as invoices.csv.

Finish with a Final step declaring invoiceCount, totalAmount, processed and file — the numbers someone will want to sanity-check before opening the file.

What happens at run time

The Debug panel after a successful batch run

The whole pass takes well under a second for a few hundred rows, because nothing here calls a model — it is parsing, arithmetic and file writing. The run trace shows each step with its real input and output, so a wrong total is traceable to the row that caused it.

The task ends Completed, with the CSV under Output files.

What you get

  • A validated file, produced the same way every month.
  • A run trace per month, so "what did we run on the March export" is answerable a year later.
  • A Reports tab that breaks spend down by customer over time, straight from the invoice_processed events — no extra work, because the loop already recorded it.

Extensions

Read the PDFs, not just the JSON. Attach the supplier PDFs to the task and add a Read File step plus a Smart Agent with structured output to extract { supplier, invoiceNumber, amount, dueDate } per document. The loop stays exactly the same.

Produce a real workbook. Swap the CSV builder for JSON to Excel when finance wants formulas and multiple sheets.

Gate the payment run. If the flow is going to submit anything, put a User Approval step in front of it showing the total and the count. Approvals pause the run and resume it in place — see ticket triage for how that behaves.

Run it on a schedule. Replace the Task trigger with a Schedule trigger once the input arrives somewhere predictable.

Pitfalls

The accumulator that resets. Creating the counter inside the loop means it is recreated every iteration. Create it before.

Template braces on the array variable. The Each step takes invoices, not {{invoices}}.

Assuming the loop failed loudly. An unresolvable array path is a no-op with an error in the step result, not a failed run. Check the count on the Final step.

Sending an unbounded loop at a model. If you add an agent call inside the loop, 400 invoices means 400 model calls. Batch them, or classify once over the whole set instead.

Studio · Pulse — Cognipeer product documentation