Receipt Ledger
ivy.agent.receipt-ledger · v1.1.0
ivyx
Finds the receipts and invoices in an inbox, has a model read the vendor, date, total and currency out of each, checks the rows, sums them per currency and writes a ledger.
Steps
In the order the agent's file lists them, each with where its inputs come from and the values the file fixes. A branch or a loop is a step too.
- 01Ivy NodeFind the receiptsivy.node.mail-fetch
Reads the newest messages of a mailbox folder that match a query over IMAP and returns each one's sender, subject, date and plain text, newest first.
- takes
usernamefrom the agent's inputqueryfrom the agent's inputlimitfrom the agent's inputimap_hostfrom the agent's inputimap_portfrom the agent's inputuse_sslfrom the agent's input- set
imap_hostimap.gmail.comimap_port993use_ssltruequerysubject:invoice|receiptlimit50- secret
mail_passwordthe stored secret mail-password
- 02Model turnRead each receipt
Each message in the context is a receipt or an invoice. For each one return a row with vendor (the company that was paid or that issued the invoice), date (the invoice or payment date as YYYY-MM-DD), total (the total amount as a number, without thousands separators or currency signs), currency (the three-letter code, EUR for euros, USD for dollars) and subject (copied from the message). Use only what the message says; do not leave any message out.
- takes
contextfrom step 1,messages
- 03Ivy NodeCheck each rowivy.node.rows-validate
Checks every row against a JSON Schema and splits the rows into valid ones and invalid ones with their problems.
- takes
rowsfrom step 2,output.rows- set
schema{"type":"object","required":["vendor","date","total","currency"],"properties":{"vendor":{"type":"string","minLength":...- returns
receipts,rejected
- 04Ivy NodeSum per currencyivy.node.table-aggregate
Groups rows and sums, averages, counts or takes the minimum or maximum of one column, optionally by month, year or day of a date column.
- takes
rowsfrom step 3,valid_rows- set
group_bycurrencyvaluetotalopsum- returns
totals
- 05Ivy NodeLay out the receiptsivy.node.markdown-table
Formats rows as a Markdown table, with the columns in the order given or as they first appear.
- takes
rowsfrom step 3,valid_rows- set
columns["date","vendor","total","currency","subject"]- returns
count
- 06Ivy NodeLay out the sumsivy.node.markdown-table
Formats rows as a Markdown table, with the columns in the order given or as they first appear.
- takes
rowsfrom step 4
- 07Ivy NodeAssemble the ledgerivy.node.template-render
Fills {{ name }} placeholders in a text template from a mapping, and reports which names were missing.
- takes
values.mailboxfrom the agent's input,usernamevalues.countfrom step 1,countvalues.receiptsfrom step 5,markdownvalues.totalsfrom step 6,markdown- set
template# Receipt ledger {{ count }} receipts read from {{ mailbox }}. ## Receipts {{ receipts }} ## Totals per currency {{ t...on_missingempty- returns
ledger
- 08Ivy NodeWrite the ledgerivy.node.file-write
Writes text to a file, creating its folder, and returns the absolute path and the number of bytes written.
- takes
textfrom step 7- set
pathreports/receipts.mdoverwritetrue- returns
path
Nodes it brings
Adding this agent in IVYX Studio adds these nodes with it. A node your workspace already has is kept as it is, even at another version.
What it touches
Collected from what each of its nodes declares, plus the model call when a step is a model turn. A declaration is the author's statement, and it is what policy rules select on.
What it needs
- A stored secret named
mail-password. The agent's file carries the name, never the value.
Inputs
| Field | Type | Description |
|---|---|---|
| usernamerequired | string | The mailbox address, which is the login name; its password is the stored secret mail-password. |
| query | string | Which mail holds the receipts: subject:invoice|receipt by default; add since:<days> or from:<name>. |
| limit | integer | At most this many messages, the newest. |
| imap_host | string | The IMAP server; imap.gmail.com when left out. |
| imap_port | integer | The IMAP port; 993 when left out. |
| use_ssl | boolean | Connect to IMAP over SSL; on when left out. |
Outputs
| Field | Type | Description |
|---|---|---|
| receipts | array | One row per receipt: vendor, date, total, currency and subject. |
| totals | array | The sum of the totals per currency. |
| count | integer | How many receipts were read. |
| rejected | integer | How many of the model's rows lacked a field. |
| ledger | string | The ledger that was written. |
| path | string | The file it was written to. |
Tests
2 of 2 test cases passed on Oct 7, 2026, in the publisher's own environment, before this version was published. The registry keeps that record; it does not run the cases again.
Requires: python:3.9, a mailbox over IMAP (Gmail unless imap_host says otherwise) with its password stored as the secret mail-password, for the cases, GreenMail on 127.0.0.1:3143/3025 (mkdata-mail.py), a model core, the three [ivyx-test] receipts of mkdata-mail.py
- three-receipts
Two receipts in euros and one in dollars: three rows, the euro sum 1470 and the dollar sum 19.5.
- given
imap_host127.0.0.1imap_port3143use_sslfalseusernameagent@ivyx.testquerysubject:invoice|receipt subject:[ivyx-test]limit50- expects
countequals 3rejectedequals 0ledgermatches ^(?=[\s\S]*1[,.]?428)(?=[\s\S]*19[.,]50?\b)(?=[\s\S]*\|\s*42\s*\|)(?=[\s\S]*1[,.]?470)totalscontains {"currency":"USD","total":19.5}pathmatches reports/receipts\.md$
- one-invoice
A query naming one invoice reads one row.
- given
imap_host127.0.0.1imap_port3143use_sslfalseusernameagent@ivyx.testquerysubject:INV-2026-0417 subject:[ivyx-test]limit50- expects
countequals 1ledgermatches Blue Harbor