> ## Documentation Index
> Fetch the complete documentation index at: https://docs.anyformat.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Bank statement processing

> Extract the statement header and every transaction as a row, run many statements through one workflow, and get the data out

This workflow reads a bank statement and returns the header (account, period, balances) as scalar fields and the transactions as rows of an `object` field. Create it once, then run every statement through it.

<div className="af-payoff">
  <div className="af-payoff-pane">
    <div className="af-payoff-cap">The statement</div>
    <div className="af-payoff-line"><b>BANK STATEMENT</b></div>

    <div className="af-payoff-line">
      <span className="af-mark">
        {'Account holder: Jordan Reyes'}
      </span>
    </div>

    <div className="af-payoff-line">
      {'Account: ****4471 · 2026-03-01 to 2026-03-31'}
    </div>

    <div className="af-payoff-line">
      {'2026-03-02  Salary - Brightline Ltd   +3,200.00'}
    </div>

    <div className="af-payoff-line">
      {'2026-03-04  Rent - Harbor Apartments  -1,650.00'}
    </div>

    <div className="af-payoff-line">
      <span className="af-mark">
        {'Closing balance: 2,983.35'}
      </span>
    </div>
  </div>

  <div className="af-payoff-arrow">→</div>

  <div className="af-payoff-pane">
    <div className="af-payoff-cap">What comes back</div>
    <div className="af-payoff-line"><span className="af-payoff-key">account\_holder</span><b>{'Jordan Reyes'}</b>{'  '}<span className="af-cite">99%</span></div>
    <div className="af-payoff-line"><span className="af-payoff-key">account\_number</span><b>{'****4471'}</b>{'  '}<span className="af-cite">99%</span></div>
    <div className="af-payoff-line"><span className="af-payoff-key">closing\_balance</span><b>{'2983.35'}</b>{'  '}<span className="af-cite">99%</span></div>
    <div className="af-payoff-line"><span className="af-payoff-key">transactions</span><b>{'6 rows'}</b></div>

    <div className="af-payoff-line">
      <span className="af-cite">
        {'↳ each row: date, description, amount, type'}
      </span>
    </div>
  </div>
</div>

<div className="af-cost">
  <span><b>Nodes</b><strong>Parse → Extract</strong></span>
  <span><b>Credits</b><strong>60 per page</strong>{' (Parse 25 + Extract 35, default tiers)'}</span>
  <span><b>You get</b>{' 6 header fields + one row per transaction'}</span>
</div>

## The graph

<CodeGroup>
  ```json Graph theme={null}
  {
    "name": "Bank statement processor",
    "nodes": [
      { "id": "parse_1", "type": "parse" },
      {
        "id": "extract_1",
        "type": "extract",
        "extraction_schema": {
          "fields": [
            { "name": "account_holder",         "description": "Name of the account holder as shown on the statement", "data_type": "string" },
            { "name": "account_number",         "description": "Bank account number, may be partially masked",         "data_type": "string" },
            { "name": "statement_period_start", "description": "First day of the statement period",                    "data_type": "date" },
            { "name": "statement_period_end",   "description": "Last day of the statement period",                     "data_type": "date" },
            { "name": "opening_balance",        "description": "Account balance at the start of the period",           "data_type": "float" },
            { "name": "closing_balance",        "description": "Account balance at the end of the period",             "data_type": "float" },
            {
              "name": "transactions",
              "description": "Every transaction listed on the statement, one row each",
              "data_type": "object",
              "nested_fields": [
                { "name": "date",        "description": "Date of the transaction",                                              "data_type": "date" },
                { "name": "description", "description": "Transaction description or memo",                                      "data_type": "string" },
                { "name": "amount",      "description": "Transaction amount: positive for deposits, negative for withdrawals",   "data_type": "float" },
                {
                  "name": "type",
                  "description": "Type of transaction",
                  "data_type": "enum",
                  "enum_options": [
                    { "name": "deposit",    "description": "Incoming deposit" },
                    { "name": "withdrawal", "description": "Outgoing withdrawal" },
                    { "name": "fee",        "description": "Bank fee or charge" },
                    { "name": "transfer",   "description": "Transfer between accounts" },
                    { "name": "interest",   "description": "Interest earned or charged" }
                  ]
                }
              ]
            }
          ]
        }
      }
    ],
    "edges": [{ "source": "parse_1", "target": "extract_1" }]
  }
  ```

  ```python Python theme={null}
  import os
  from anyformat.sdk import Client
  from anyformat.workflow import Schema

  client = Client(api_key=os.environ["ANYFORMAT_API_KEY"])

  workflow = (
      client.workflow("Bank statement processor")
      .parse()
      .extract([
          Schema.string("account_holder",       "Name of the account holder as shown on the statement"),
          Schema.string("account_number",       "Bank account number, may be partially masked"),
          Schema.date("statement_period_start", "First day of the statement period"),
          Schema.date("statement_period_end",   "Last day of the statement period"),
          Schema.float("opening_balance",       "Account balance at the start of the period"),
          Schema.float("closing_balance",       "Account balance at the end of the period"),
          Schema.object("transactions", "Every transaction listed on the statement, one row each", fields=[
              Schema.date("date",          "Date of the transaction"),
              Schema.string("description", "Transaction description or memo"),
              Schema.float("amount",       "Transaction amount: positive for deposits, negative for withdrawals"),
              Schema.enum("type", "Type of transaction", options=[
                  Schema.option("deposit",    "Incoming deposit"),
                  Schema.option("withdrawal", "Outgoing withdrawal"),
                  Schema.option("fee",        "Bank fee or charge"),
                  Schema.option("transfer",   "Transfer between accounts"),
                  Schema.option("interest",   "Interest earned or charged"),
              ]),
          ]),
      ])
      .create()
  )
  ```

  ```typescript TypeScript theme={null}
  import { Anyformat, Schema } from "@anyformat/sdk";

  const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });

  const workflow = await af
    .workflow("Bank statement processor")
    .parse()
    .extract([
      Schema.string("account_holder",       "Name of the account holder as shown on the statement"),
      Schema.string("account_number",       "Bank account number, may be partially masked"),
      Schema.date("statement_period_start", "First day of the statement period"),
      Schema.date("statement_period_end",   "Last day of the statement period"),
      Schema.float("opening_balance",       "Account balance at the start of the period"),
      Schema.float("closing_balance",       "Account balance at the end of the period"),
      Schema.object("transactions", "Every transaction listed on the statement, one row each", [
        Schema.date("date",          "Date of the transaction"),
        Schema.string("description", "Transaction description or memo"),
        Schema.float("amount",       "Transaction amount: positive for deposits, negative for withdrawals"),
        Schema.enum("type", "Type of transaction", [
          Schema.option("deposit",    "Incoming deposit"),
          Schema.option("withdrawal", "Outgoing withdrawal"),
          Schema.option("fee",        "Bank fee or charge"),
          Schema.option("transfer",   "Transfer between accounts"),
          Schema.option("interest",   "Interest earned or charged"),
        ]),
      ]),
    ])
    .create();
  ```
</CodeGroup>

## Run and read

One statement is one run. Keep each file in its own call so each statement gets its own result; a call that carries several files makes one document packet and one run.

<CodeGroup>
  ```bash curl theme={null}
  for f in statement-jan.pdf statement-feb.pdf statement-mar.xlsx; do
    curl -X POST "https://api.anyformat.ai/v3/workflows/$WORKFLOW_ID/upload/run/" \
      -H "Authorization: Bearer $ANYFORMAT_API_KEY" \
      -F "files=@$f"
    # -> 202 {"run_id": "...", "status": "queued"}; keep each run_id
  done

  # Read each run. Repeat until "status" is "processed".
  curl "https://api.anyformat.ai/v3/runs/$RUN_ID/" \
    -H "Authorization: Bearer $ANYFORMAT_API_KEY"
  ```

  ```python Python theme={null}
  for path in ["statement-jan.pdf", "statement-feb.pdf", "statement-mar.xlsx"]:
      result = workflow.run(path).wait()
      print(result.field("account_holder").value, result.field("closing_balance").value)

      # An object field is a list of rows; each row maps a column name to a field
      for row in result.fields["transactions"]:
          if row["type"].value == "fee":
              print("fee:", row["description"].value, float(row["amount"].value))
  ```

  ```typescript TypeScript theme={null}
  import { readFile } from "node:fs/promises";
  import type { ExtractedField, ExtractedRows } from "@anyformat/sdk";

  for (const path of ["statement-jan.pdf", "statement-feb.pdf", "statement-mar.xlsx"]) {
    const file = new File([await readFile(path)], path);
    const run = await workflow.run(file);
    const result = await run.wait();

    // field() returns a scalar or rows; narrow it to what the schema declares
    const scalar = (name: string) => result.field(name) as ExtractedField<string> | undefined;
    console.log(scalar("account_holder")?.value, scalar("closing_balance")?.value);

    // An object field is an array of rows; each row maps a column name to a field
    const rows = result.field("transactions") as ExtractedRows;
    for (const row of rows) {
      if (row.type?.value === "fee") console.log("fee:", row.description?.value, Number(row.amount?.value));
    }
  }
  ```
</CodeGroup>

## What comes back

One statement with six transactions, trimmed to two header fields and two rows. The run took 21 seconds.

```json Response highlight={8,9} theme={null}
{
  "results": {
    "extractions": [
      { "fields": {
        "account_holder":  { "value": "Jordan Reyes", "confidence": 99.0, "evidence": [{ "text": "Account holder: Jordan Reyes", "page_number": 1 }] },
        "closing_balance": { "value": "2983.35", "confidence": 99.0, "evidence": [] },
        "transactions": [
          { "date": { "value": "2026-03-02", "confidence": 99.0 }, "description": { "value": "Salary - Brightline Ltd",  "confidence": 99.0 }, "amount": { "value": "3200.0",  "confidence": 99.0 }, "type": { "value": "deposit",    "confidence": 95.0 } },
          { "date": { "value": "2026-03-04", "confidence": 99.0 }, "description": { "value": "Rent - Harbor Apartments", "confidence": 99.0 }, "amount": { "value": "-1650.0", "confidence": 99.0 }, "type": { "value": "withdrawal", "confidence": 99.0 } },
          "…"
        ] } }
    ] } }
```

Every field also carries `verification_status` and `value_override`. [Runs and results](/concepts/runs-and-results) has the full envelope, section by section.

Read the fields from `extractions[0].fields`. A scalar field is one object with `value`, `confidence` and `evidence`. An `object` field is an array of rows, and every cell in a row is that same object. Every value is a string on the wire: numbers as `"2983.35"`, dates as `"2026-03-31"`, booleans as `"True"` or `"False"`. The SDKs keep them as strings too, so convert before you add them up. A field the model could not find comes back as `{"value": null, "confidence": null, "evidence": []}`.

## Getting the data out

* **In the app**, open the workflow and export the results as CSV, Excel or JSON. Each statement is a row and `transactions` expands into its own sheet or nested array. See [Outputs](/concepts/outputs).
* **Over the API**, read each run as shown above, or list a workflow's runs for a date range and flatten them yourself. [Daily CSV export](/examples/daily-csv-export) is a complete script for that.

## When it goes wrong

**Rows are missing from a long transaction list.** A statement runs to hundreds of rows across several pages, and a single standard pass can drop one in a dense table. Ask for the totals the statement prints (`total_deposits`, `total_withdrawals`) as extra fields and compare them with the sum of the rows. A mismatch is the cheapest signal that a row was missed. When the tables stay dense, move the Parse node to the agentic tier with `"mode": "agentic"` at 100 credits per page. [Agentic parse to markdown](/examples/agentic-parse-to-markdown) shows the presets.

**Withdrawals come back positive.** `amount` is a `float`, and nothing in the type says which direction the money went. Put the convention in the description: "positive for deposits, negative for withdrawals". The sign then holds across banks that print a Debit column instead of a minus sign.

**A transaction type comes back empty.** `type` is an `enum`, so it returns only the names you declared. A statement that prints "standing order" or "card payment" has nothing to match among deposit, withdrawal, fee, transfer and interest. Add the option, or use a `string` field when the bank's vocabulary is open.

**Your balances do not add up.** Every value arrives as a string: `"2983.35"`, `"-1650.0"`, `"2026-03-31"`. Cast each one to the type you declared before you do arithmetic on it. A field the model could not find comes back as `{"value": null, "confidence": null, "evidence": []}`, so test for `null` before you cast.

**A batch of statements fails halfway through.** Upload and run is limited to 60 requests per minute. Submit statements serially with a short pause between them, or spread them across workers. Feed the workflow the XLSX or CSV the bank already offers when you have the choice: the cells are structured and no OCR runs.

## Next steps

<CardGroup cols={2}>
  <Card title="Extract node" icon="table-list" href="/guides/nodes/extract">
    Object fields, modes and smart lookup
  </Card>

  <Card title="Outputs" icon="file-export" href="/concepts/outputs">
    CSV, Excel and JSON exports from the app
  </Card>

  <Card title="Daily CSV export" icon="file-csv" href="/examples/daily-csv-export">
    Flatten a day of runs into one CSV over the API
  </Card>

  <Card title="Runs and results" icon="list" href="/concepts/runs-and-results">
    The model behind the envelope
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.