Bank Statement Reader
ivy.agent.bank-statement · v1.1.0
ivyx
Finds the newest bank statement that came by mail, saves the PDF, has a model read the balances and every transaction out of it in any language, checks the rows, and writes a report.
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 statement mailivy.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 inputattachments_dirfrom 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:statementlimit1save_attachmentstrue- secret
mail_passwordthe stored secret mail-password- returns
pdf
- 02For eachRead each PDF
Runs PDF Text once for each item of attachment_paths of step 1.
- 03Model turnRead the statement
The context holds the text of a bank statement, in any language and layout. Return statement, an object with exactly these keys: statement_number (the statement's number), date (the statement date, or the last day of its period, as YYYY-MM-DD), account (the account number), holder (the account holder), previous_balance (the opening or previous balance), total_debits (the money out, as the statement totals it), total_credits (the money in, as the statement totals it), new_balance (the closing or new balance), fees (the bank's charges, 0 when none are shown) and orders (how many transactions). Return transactions: one row per transaction, in the statement's order, with n (1, 2, 3 ...), counterparty (who was paid or who paid; the description when no name is given), account (theirs, empty when not shown), date (YYYY-MM-DD), debit (money out, as a number, 0 when none), credit (money in, as a number, 0 when none), code (the statement's code for it, empty when none) and purpose (what it was for). Write every amount as a plain number: no thousands separators, no currency. When the statement gives no total of money out or in, add the transactions. Use only what the text says and do not leave any transaction out.
- takes
contextfrom step 2,results- returns
statement
- 04Ivy NodeCheck each transactionivy.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 3,output.transactions- set
schema{"type":"object","required":["n","counterparty","date","debit","credit"],"properties":{"n":{"type":"integer","minimum...- returns
transactions,rejected,problems
- 05Ivy NodeLay out the transactionsivy.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,valid_rows- set
columns["n","date","counterparty","debit","credit","purpose"]- returns
count
- 06Ivy NodeAssemble the reportivy.node.template-render
Fills {{ name }} placeholders in a text template from a mapping, and reports which names were missing.
- takes
values.statementfrom step 3,output.statementvalues.countfrom step 5,row_countvalues.ordersfrom step 5,markdown- set
template# Statement {{ statement.statement_number }} of {{ statement.date }} Account {{ statement.account }} ({{ statement.ho...on_missingempty
- 07Ivy NodeWrite the reportivy.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 6- set
pathreports/bank-statement.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 statement: subject:statement by default; name the bank's own word, such as subject:izvod or subject:ekstre, or add from:<bank>. |
| attachments_dir | string | Where the PDF is saved, relative to the workspace. |
| 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 |
|---|---|---|
| statement | object | statement_number, date, account, holder, previous_balance, total_debits, total_credits, new_balance, fees, orders. |
| transactions | array | One row per transaction: n, counterparty, account, date, debit, credit, code, purpose. |
| count | integer | How many transactions were read. |
| rejected | integer | How many of the model's rows lacked a field. |
| problems | array | The rejected rows, each with its problems as one sentence. |
| array | The statement files saved from the mail. | |
| path | string | The report that was written. |
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 two [ivyx-bank] statement mails of mkdata-mail.py (data/bank/, made up)
- izvod-42
A made-up Montenegrin statement (izvod 42 of 15.09.2026): five orders, 1000.00 before, 368.50 out, 1200.00 in, 1831.50 after.
- given
imap_host127.0.0.1imap_port3143use_sslfalseusernameagent@ivyx.testquerysubject:izvod subject:[ivyx-bank]- expects
countequals 5rejectedequals 0statement.statement_numbermatches ^0*42$statement.previous_balanceequals 1000statement.total_debitsequals 368.5statement.total_creditsequals 1200statement.new_balanceequals 1831.5pdfmatches statement-42\.pdfpathmatches reports/bank-statement\.md$
- english-7
A made-up English statement in another layout, with a balance column and thousands separators: five transactions, 2,500.00 to 4,499.81.
- given
imap_host127.0.0.1imap_port3143use_sslfalseusernameagent@ivyx.testquerysubject:"account statement" subject:[ivyx-bank]- expects
countequals 5rejectedequals 0statement.statement_numbermatches ^0*7$statement.previous_balanceequals 2500statement.total_debitsequals 1400.19statement.total_creditsequals 3400statement.new_balanceequals 4499.81pdfmatches statement-7-en\.pdf