← All ivy-agents
IVYXSTUDIO · IVY AGENT
R

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.

  1. 01
    Find the receipts
    ivy.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
    username from the agent's input
    query from the agent's input
    limit from the agent's input
    imap_host from the agent's input
    imap_port from the agent's input
    use_ssl from the agent's input
    set
    imap_host imap.gmail.com
    imap_port 993
    use_ssl true
    query subject:invoice|receipt
    limit 50
    secret
    mail_password the stored secret mail-password
    Ivy Node
  2. 02
    Read 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
    context from step 1, messages
    Model turn
  3. 03
    Check each row
    ivy.node.rows-validate

    Checks every row against a JSON Schema and splits the rows into valid ones and invalid ones with their problems.

    takes
    rows from step 2, output.rows
    set
    schema {"type":"object","required":["vendor","date","total","currency"],"properties":{"vendor":{"type":"string","minLength":...
    returns
    receipts, rejected
    Ivy Node
  4. 04
    Sum per currency
    ivy.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
    rows from step 3, valid_rows
    set
    group_by currency
    value total
    op sum
    returns
    totals
    Ivy Node
  5. 05
    Lay out the receipts
    ivy.node.markdown-table

    Formats rows as a Markdown table, with the columns in the order given or as they first appear.

    takes
    rows from step 3, valid_rows
    set
    columns ["date","vendor","total","currency","subject"]
    returns
    count
    Ivy Node
  6. 06
    Lay out the sums
    ivy.node.markdown-table

    Formats rows as a Markdown table, with the columns in the order given or as they first appear.

    takes
    rows from step 4
    Ivy Node
  7. 07
    Assemble the ledger
    ivy.node.template-render

    Fills {{ name }} placeholders in a text template from a mapping, and reports which names were missing.

    takes
    values.mailbox from the agent's input, username
    values.count from step 1, count
    values.receipts from step 5, markdown
    values.totals from step 6, markdown
    set
    template # Receipt ledger {{ count }} receipts read from {{ mailbox }}. ## Receipts {{ receipts }} ## Totals per currency {{ t...
    on_missing empty
    returns
    ledger
    Ivy Node
  8. 08
    Write the ledger
    ivy.node.file-write

    Writes text to a file, creating its folder, and returns the absolute path and the number of bytes written.

    takes
    text from step 7
    set
    path reports/receipts.md
    overwrite true
    returns
    path
    Ivy Node

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.

Writes filesCalls a modelmessage-readNetwork

What it needs

  • A stored secret named mail-password. The agent's file carries the name, never the value.

Inputs

FieldTypeDescription
usernamerequiredstringThe mailbox address, which is the login name; its password is the stored secret mail-password.
querystringWhich mail holds the receipts: subject:invoice|receipt by default; add since:<days> or from:<name>.
limitintegerAt most this many messages, the newest.
imap_hoststringThe IMAP server; imap.gmail.com when left out.
imap_portintegerThe IMAP port; 993 when left out.
use_sslbooleanConnect to IMAP over SSL; on when left out.

Outputs

FieldTypeDescription
receiptsarrayOne row per receipt: vendor, date, total, currency and subject.
totalsarrayThe sum of the totals per currency.
countintegerHow many receipts were read.
rejectedintegerHow many of the model's rows lacked a field.
ledgerstringThe ledger that was written.
pathstringThe 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_host 127.0.0.1
    imap_port 3143
    use_ssl false
    username agent@ivyx.test
    query subject:invoice|receipt subject:[ivyx-test]
    limit 50
    expects
    count equals 3
    rejected equals 0
    ledger matches ^(?=[\s\S]*1[,.]?428)(?=[\s\S]*19[.,]50?\b)(?=[\s\S]*\|\s*42\s*\|)(?=[\s\S]*1[,.]?470)
    totals contains {"currency":"USD","total":19.5}
    path matches reports/receipts\.md$
  • one-invoice

    A query naming one invoice reads one row.

    given
    imap_host 127.0.0.1
    imap_port 3143
    use_ssl false
    username agent@ivyx.test
    query subject:INV-2026-0417 subject:[ivyx-test]
    limit 50
    expects
    count equals 1
    ledger matches Blue Harbor