> ## 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

**Nodes used:** [Parse](/guides/nodes/parse), [Extract](/guides/nodes/extract)

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.

## 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 rows. The run took 21 seconds. Every field also carries `verification_status`, `value_override` and `evidence`; only the first shows them here.

```json theme={null}
{
  "id": "06a8d903-b9f8-7914-8000-dbc02769b602",
  "status": "processed",
  "results": {
    "document_packet_id": "06a8d903-b815-7b92-8000-82f7269755f2",
    "verification_url": "https://app.anyformat.ai/workflows/.../files/...",
    "parse": { "markdown": "..." },
    "classifications": [],
    "splits": [],
    "extractions": [
      {
        "split_name": null,
        "partition": null,
        "fields": {
          "account_holder":         { "value": "Jordan Reyes", "confidence": 99.0, "verification_status": "not_verified", "value_override": null,
                                      "evidence": [{ "text": "Account holder: Jordan Reyes", "page_number": 1 }] },
          "account_number":         { "value": "****4471",   "confidence": 99.0 },
          "statement_period_start": { "value": "2026-03-01", "confidence": 99.0 },
          "statement_period_end":   { "value": "2026-03-31", "confidence": 99.0 },
          "opening_balance":        { "value": "2450.1",     "confidence": 99.0 },
          "closing_balance":        { "value": "2983.35",    "confidence": 99.0 },
          "transactions": [
            {
              "date":        { "value": "2026-03-02", "confidence": 99.0,
                               "evidence": [{ "text": "2026-03-02  Salary - Brightline Ltd  +3,200.00", "page_number": 1 }] },
              "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 }
            }
          ]
        }
      }
    ],
    "edits": []
  }
}
```

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.

## Tips

* Describe `amount` as "positive for deposits, negative for withdrawals". The sign convention then holds across banks.
* An XLSX or CSV statement gives better rows than a scanned PDF. The cells are already structured and no OCR runs.
* 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.
* Statements with dense multi-page tables are a good fit for the Agentic parse tier: set `"mode": "agentic"` on the Parse node. See [Agentic parse to markdown](/examples/agentic-parse-to-markdown).
* Upload and run is limited to 60 requests per minute. Submit statements serially with a short pause, or spread them across workers.

## 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>
