Collections Agent
An accounts-receivable agent that reads replies to payment chasers, works out what each one means, and drafts the answer for a person to approve.
- Role
- Full-stack Engineer
- Stack
- 1 × Next.js
- 1 × TypeScript
- 1 × PostgreSQL
- 1 × Drizzle
- 1 × Claude API
- 1 × Microsoft Graph
- 1 × Entra ID
- 1 × Azure
The job
The client is a Norwegian equipment-rental company, and like any rental business it has invoices that go unpaid. The finance team chases them by email, and the replies are where the work is: a promise to pay on Friday, a dispute about one line, a note that the money already went out, a threat of legal action. Each one needs reading, a decision and usually an answer.
I built the agent that does the first pass at Kinetichex. It reads each reply, decides what it means and drafts the answer. A person approves every answer it drafts.
Replies come in by polling
Chasers go out from one shared mailbox through Microsoft Graph. The app creates each message as a draft and then sends it, so it keeps the message and conversation ids for matching replies later.
Replies are collected by a job that runs every ten minutes and reads the mailbox with a Graph delta query, picking up only what changed since the last run. A reply is matched to its invoice by conversation id or In-Reply-To header first, with fallbacks when a customer starts a fresh email.
Two runs should not classify the same reply at once. Each reply is claimed in the database with FOR UPDATE SKIP LOCKED and moves from pending to processing to done, and a claim left stuck by a crashed run is picked up again after ten minutes.
Three tools and a decision
The classifier is a Claude tool-calling loop with three tools. get_invoice_details returns the invoice. read_conversation returns the thread verbatim, both directions, up to the last twenty messages. submit_decision records the result. The loop gets at most six turns.
The decision is one of twelve outcomes, from promise_to_pay and dispute_full to already_paid, legal_threat, bankruptcy and needs_human, with the next invoice status, a reason, and a draft where an answer is needed. Some rules are fixed in the prompt rather than left to judgement: every customer-facing word is Norwegian, a legal threat or a bankruptcy gets no reply, and an "already paid" claim is never closed automatically, because that is a claim to check, not a fact.
Drafts are built from editable reply templates, and admins can change the prompt from the app without a deploy.
A person between the agent and the customer
Drafts land in a queue that only admins can act on. They can approve, resolve, snooze or reassign each item. On approval, the body and recipients the reviewer edited are what go out through Graph, and both the original draft and the sent text are kept, so it is always clear what the agent proposed and what a person changed.
The queue is built to be clicked twice without harm. Approvals run in a transaction with a compare-and-swap on the item's state, so two reviewers cannot send the same answer. If the email has gone but logging it fails afterwards, the item is not reported as a failed send, so a reviewer is not prompted to approve it again and send it twice. Actions are written to an activity log.
Bugs that would have sent the wrong thing
Edits that never left. Reviewers could edit a draft, but the send path read the original. The fix made the edited version the one sent, and made group approvals atomic.
A sync that undid corrections. Contact details came from a daily sync with the company's data platform, which overwrote the corrections the team made in the app. Corrections now live in their own override table that the sync leaves alone.
A greeting for the wrong person. A placeholder draft greeted the company's own coordinator by name instead of the customer. Small, and exactly the kind of mistake a customer notices.
Microsoft sign-in, mail and Azure
Staff sign in with Microsoft Entra ID, limited to the company's email domain. New accounts wait as pending until an admin approves them, and only admins can act on the queue.
The app runs in a Docker container on Azure Container Apps, and GitHub Actions deploys it on every push to main, signing in to Azure with OIDC rather than stored keys. The database is Azure Postgres, reached with a managed identity instead of a password.
Questions
- What is the Collections Agent?
- An accounts-receivable tool for a Norwegian equipment-rental company. It works from the company's open invoices, sends payment chasers from a shared mailbox, reads the replies that come back, decides what each reply means and drafts an answer for the finance team to approve.
- How does the agent decide what to do with a reply?
- Claude reads the reply with three tools: one returns the invoice, one returns the recent email thread, and one records the decision. The decision is one of twelve outcomes, such as a promise to pay, a dispute, a claim that the invoice is already paid, or a case that needs a person, together with the next status and a draft answer where one is needed.
- Does the agent answer customers on its own?
- No. The inbound agent only drafts. Every answer it writes waits in a queue until an admin approves it, and the version the reviewer edited is the one that gets sent.
Last updated