Arc Forge Labs Docs v2026.10.53

Job and invoice links

Flow keeps its own record of each job from the work system (Seazona) and of the accounting invoice (Xero) that bills it. This page describes how Flow makes that link. The code is in server/core/pipeline.js. Issue #58 tracks the pipeline.

The pipeline reads only Flow's own copies of the two systems. It never writes to Seazona or Xero. Matching many years of history costs no API calls.

The run

A background worker runs the pipeline every two minutes. It runs as a pipeline.run job on the job queue, so two Flow processes never run it at the same time. It does work only while an integration that feeds it is on. FLOW_PIPELINE=off stops it.

flowchart LR
  wc[Seazona clients] -- same name --> fc[Flow customers]
  xc[Xero contacts] -- same name --> fc
  wo[Seazona orders] -- billable --> job[Jobs in the ledger]
  fc --> job
  job --> m[Matcher]
  xi[Xero invoice copy] --> m
  m -- exact --> link[Linked]
  m -- likely --> sug[Suggestion for a person]
  m -- nothing, copy complete --> none[No invoice]

Each run has three steps, and then runs the automations that are on (docs/AUTOMATIONS.md): they mark jobs ready to invoice and draft them by rule (docs/INVOICING_RULES.md).

  1. Customers. A Seazona client and a Xero contact link to the Flow customer with the same name. Case, spacing, punctuation and a "Pty Ltd" end do not count. The link is made only when exactly one record on each side has the name (exact_name). Other customers wait for a person.
  2. Work. Each billable Seazona order becomes or updates its job in the billing ledger (server/core/ledger.js). The job gets the Flow customer that its client links to. A new customer link reads all jobs again, so older jobs get their customer too. An order deleted in Seazona sends its job to review.
  3. Matching. The matcher compares each job with the sales invoices in the Xero copy (docs/XERO.md, "The mirror").

The Xero adapter reads contacts and items once a day, so that customers can link without a person.

The matcher

The matcher checks a job when the job is new, when Seazona changed it, or when the Xero copy changed. Only exact evidence links a job by itself.

Step Evidence Outcome
1 The job is linked already Flow follows the invoice: void, delete and paid change the job
2 Flow sent the job's invoice to Xero Linked, flow_invoice
3 The earlier installation linked the order (prior_links), and the invoice is in the copy with an amount that fits, or a person made that link Linked, prior_link
4 One Xero invoice of the customer has the Seazona invoice number (a trailing R does not count) Linked, source_invoice_ref
5 The customer's invoices from 7 days before to 45 days after the job date Scored

The job date is the Seazona invoice date. When there is none, it is the order's due date, then its order date.

The score

Signal Weight Meaning
total 6 The invoice total equals the job total
total_gst 5 The invoice total equals the job subtotal, or the subtotal plus 10%
items_same 3 Both have the same item codes
items_overlap 1 They share some item codes
date_close 2 The dates are 3 days apart or less
date_near 1 The dates are 14 days apart or less
prior_link 4 The earlier installation linked this pair, at another amount
not_live -3 The invoice is voided or deleted
closer_job -4 Another unlinked job of the customer, at an amount that fits, is closer in date

A candidate counts only with a total signal or prior_link. An invoice for the same customer at another amount is other work.

Result Rule The job
Suggested The top score is 8 or more, and 3 or more above the next ambiguous, with the suggestion first
Unsure Any other candidates ambiguous, with up to 5 suggestions
No invoice No candidate, and the copy is complete and was read in the last day not_found
Wait No candidate, and the copy is not complete or not fresh, or the job has no date No change
No contact The customer has no Xero contact link No change

Each suggestion is a row in link_suggestions, with its score and its signals, so a person can see why. A later run marks a suggestion that it does not make again stale. A person's decision (accepted, rejected) stays.

A row in link_rejections is a person's "not this one". The matcher never offers that pair again.

The amount of an invoice that linked jobs already account for is taken. An invoice whose whole total is taken is not a candidate for another job.

A person settles it

A job with no invoice whose evidence is ambiguous shows a warning icon and "Needs invoice" on its panel, one line, "This job has no invoice.", and two buttons.

Button What Flow does
Make invoice Rejects every open suggestion, records the job as not_found (by a person), then drafts its invoice. The live check before the draft still runs.
Reconcile Opens the Reconcile page at this job.

Reconcile

The Reconcile page (/reconcile, under Work) matches Flow's own records: Flow jobs against Flow invoices. It calls no integration. Whether a Flow invoice matches an invoice in an accounting system is that integration's own work, on its own page.

It is laid out like Jobs and Invoices. /reconcile lists every gap, oldest first, jobs and invoices together:

Gap A gap is
Job with no invoice A job marked ready, with no Flow invoice, not held, and not tied to an accounting invoice by the matcher.
Invoice with no job A Flow invoice, not void, that no job is linked to.

The Filter chip is the one Jobs uses (server/ui/view-bar.js): groups of rows of field, operator and value, joined by And / Or, as on ClickUp. The fields are Type (Job or Invoice), Status, Client, Practitioner, Date, Total, Match (has a possible match, or none) and the lab's custom fields. Status is the job's status from the lab system, or the invoice's stage. Practitioner and custom fields belong to jobs, so a row on them never hides an invoice. The rows are kept for each viewer in the browser.

The Invoices list has the same chip at the right of its tabs. Its fields are Client, Number, Reference, Date, Total and Problem; the tabs stay the status. It filters the rows of the tab shown, and is kept in the browser. The search icon at the right of the toolbar opens a box that finds a client, practitioner, invoice number or amount. Jobs and Invoices use the same icon (findBox in server/ui/view-bar.js); the sidebar search (Ctrl+K) stays the way to find anything in Flow. Ticked jobs get draft invoices from the bulk bar ("Make invoices"); a ticked job with a possible invoice is left for a person to check.

A row opens that gap at /reconcile/:id (?kind=invoice for an invoice), as an invoice opens from Invoices: the pager on top ("← Back" to the list, "n of N" of the list as it was filtered, previous and next), then the gap. The search icon for another invoice (or job) sits at the right of the toolbar over the compare table.

For one gap the page shows the record beside its possible other side, best first: the same client within 60 days, ranked by total, lines, Reference and date. The search icon finds any other: Flow invoices by number, client, Reference or amount; jobs by client, practitioner or case. Each fact has a mark against the record's: ✓ same, ≠ different, none when one side does not know.

Fact Compared
Client The same contact.
Reference The Reference Flow would give the job's invoice, against the invoice's.
Date Days between the job date and the invoice date. ✓ within 3 days.
Lines Price items and amounts before tax, in any order.
Total Equal.
Other jobs Jobs already linked to the invoice.
Status The invoice's status, or the job's stage. No mark.

"Link this invoice" (or "Link this job") sets the job's Flow invoice. Every open matcher suggestion for the job is rejected, and a job held on an unclear accounting match is settled. "Make invoice" drafts the job's Flow invoice; for an unclear job it first records "no invoice" (confirmNoInvoice). After either, the next gap opens. Skip changes nothing. A phone shows one card per candidate instead of the table.

API: GET /api/invoicing/reconcile (both queues), GET /api/invoicing/jobs/:id/reconcile and GET /api/invoicing/invoices/:id/reconcile (one record and its candidates; ?q= searches), POST /api/invoicing/jobs/:id/no-invoice and POST /api/invoicing/jobs/:id/link ({ invoiceId }, a Flow invoice). The two POSTs need an owner or admin. Code: listReconcile, jobMatchDetail and invoiceMatchDetail in server/core/job-match.js; confirmNoInvoice and linkJobToInvoice in server/core/ledger.js.

Old jobs

A job with no invoice whose job date is more than 90 days ago is held, once, with the reason. It may be work billed some other way. A person who releases it has decided, and the matcher does not hold it again.

What comes next

  • Find an invoice by hand in the linking tool, beyond the suggestions.
  • Credit notes and payments on the linked job (step 3).
  • Draft rules and send modes are built: docs/INVOICING_RULES.md.