Guide

Write your own report

Write a User report as a .rhai script in the report directory, check that report.list shows it, then run it with report.run and export it with report.export.

GnuCash comes with reports of its own, and red-ale has translated most of them. Others, such as GnuCash's business reports and the sample reports you may have written for it, are left for you to write. A User report is a Rhai script, a file ending in .rhai in the report directory. red-ale loads it each time a report method runs, lists it beside the built-in reports, and runs it in a sandbox: it can read the open book but cannot change it, read other files, reach the network or ask the clock.

You can write one yourself, or ask an AI agent to. Either way the steps are the same, and the script is an ordinary text file. There is no method that takes a script's text, so the file must be put in the report directory by something that can write files on the machine where red-ale runs: you, your editor, or an agent with file access.

Steps

  1. Choose the report directory and tell red-ale where it is, with config.set: {"changes": {"reports.directory": "/home/me/red-ale-reports"}}. config.get with the path reports.directory reads it back. Scripts may be in sub-directories; at most 1,000 files of at most 1 MiB are read.
  2. Write the script. Save the one below as asset-balances.rhai in that directory. A script defines three functions: templates() names the reports it provides, options() declares the options a user can set, and render() builds the report from the book.
  3. Check that it loaded. Call report.list. The new report is in reports with "kind": "user" and its script's path. If the script has a mistake, it is not listed; instead problems names the file, line and column and says what is wrong (report-load).
  4. Run it with report.run, by name. You get the report result as data, with exact figures. A fault at run time is a report-script error with the line and column. Edit the file and run it again: there is nothing to restart.
  5. Export it with report.export for a page you can read or print.
  6. Share the options. An option you declare shows up in report.options and can be set in options, like those of the built-in reports. Save a favourite set with report.saveConfiguration (Run and export a report).

A minimal script

This report lists every asset account that can hold transactions, with its balance now:

// Asset Balances: every asset account that can hold transactions, with its balance now.

fn templates() {
    [#{ guid: "5a1e0c9d2b7a4e3f8a5b1c2d3e4f5a6b", name: "Asset Balances", menuPath: ["_Custom"] }]
}

fn options(params, o, book) {
    o.string("General", "Report Title", "a", "Title for this report.", "Asset Balances");
}

fn render(params, book, opts, ctx) {
    let rows = book.accounts()
        .filter(|a| a.type == "ASSET" && !a.placeholder)
        .map(|a| #{ cells: [#{ text: a.fullName }, #{ number: a.balance_as_of(ctx.now), places: 2 }] });

    #{
        title: opts.get("General", "Report Title"),
        sections: [#{ blocks: [#{
            kind: "table",
            columns: [#{ label: "Account", role: "label" }, #{ label: "Balance", role: "amount" }],
            rows: rows
        }] }]
    }
}
  • The guid is any 32 lowercase hex digits you make up, and it must be unique among the reports. Keep it when you edit the script, so saved configurations still find it.
  • book.accounts() returns the book's accounts. a.balance_as_of(ctx.now) is an exact rational, and the number cell shows it to two places. Money is never a floating-point number in a script.
  • render() returns a map of the report result: a title and sections of blocks (tables, text, charts).

Running it on a book whose ASSET account Root:Asset:Bank holds 2,186.00 gives a table whose rows look like this (shown, not run, since an Example cannot put a file in the directory):

{"role": "data", "cells": [{"text": "Root:Asset:Bank"}, {"number": {"num": "2186", "denom": "1"}, "places": 2, "display": "2,186.00"}]}

The engine's whole interface, with the reads a script can make, the option types, the arithmetic and the limits, is on the Report scripts concept page. The built-in reports are written the same way and are worked examples.

Example

A directory that does not exist yet is reported under problems. Setting the directory and listing the reports works on any book. A script in the directory would appear in this list: "kind": "builtin" marks the reports that come with red-ale, and a User report has "kind": "user".

Example: with a copy of the reports/test-data example book open as book.gnucash, at 2024-03-15T12:00:00Z.

JSON-RPC

Request:

{"jsonrpc":"2.0","id":1,"method":"config.set","params":{"changes":{"reports.directory":"reports"}}}

Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "values": {
      "reports.directory": "reports"
    },
    "clearedKeys": [],
    "configFile": null
  }
}

Request:

{"jsonrpc":"2.0","id":2,"method":"report.list","params":{}}

Response:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "reports": [
      {
        "name": "Account Summary",
        "kind": "builtin",
        "…": "…"
      },
      "…"
    ],
    "problems": "…"
  }
}

Request:

{"jsonrpc":"2.0","id":3,"method":"report.run","params":{"report":"Account Summary"}}

Response:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "title": "…",
    "report": {
      "name": "Account Summary",
      "…": "…"
    },
    "sections": "…",
    "…": "…"
  }
}

Request:

{"jsonrpc":"2.0","id":4,"method":"report.export","params":{"report":"Account Summary"}}

Response:

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "title": "…",
    "html": "…"
  }
}

Command line

red-ale exec --book book.gnucash --script - <<'EOF'
config.set {"changes":{"reports.directory":"reports"}}
report.list {}
report.run {"report":"Account Summary"}
report.export {"report":"Account Summary"}
EOF

It prints:

[
  {
    "values": {
      "reports.directory": "reports"
    },
    "clearedKeys": [],
    "configFile": null
  },
  {
    "reports": [
      {
        "name": "Account Summary",
        "kind": "builtin",
        "…": "…"
      },
      "…"
    ],
    "problems": "…"
  },
  {
    "title": "…",
    "report": {
      "name": "Account Summary",
      "…": "…"
    },
    "sections": "…",
    "…": "…"
  },
  {
    "title": "…",
    "html": "…"
  }
]

MCP tool call

Tool call:

{"name":"config.set","arguments":{"changes":{"reports.directory":"reports"}}}

Result:

{
  "structuredContent": {
    "values": {
      "reports.directory": "reports"
    },
    "clearedKeys": [],
    "configFile": null
  }
}

Tool call:

{"name":"report.list","arguments":{}}

Result:

{
  "structuredContent": {
    "reports": [
      {
        "name": "Account Summary",
        "kind": "builtin",
        "…": "…"
      },
      "…"
    ],
    "problems": "…"
  }
}

Tool call:

{"name":"report.run","arguments":{"report":"Account Summary"}}

Result:

{
  "structuredContent": {
    "title": "…",
    "report": {
      "name": "Account Summary",
      "…": "…"
    },
    "sections": "…",
    "…": "…"
  }
}

Tool call:

{"name":"report.export","arguments":{"report":"Account Summary"}}

Result:

{
  "structuredContent": {
    "title": "…",
    "html": "…"
  }
}