Files
grist-accounting/references/workflows.md
Bill Ballou fdb813d647 Add bank reconciliation workflow and templates
New capability for importing bank transactions (Schwab JSON), matching
against existing ledger entries, creating missing transactions, and
reconciling balances. Includes two new Grist tables (Reconciliations,
BankRules), two bank-import templates, and reference documentation.
2026-02-19 20:26:48 -05:00

12 KiB

Workflow Examples

Detailed code examples for common accounting operations.

Important: Before adding records, consult schema.md to identify writable fields. Formula columns (marked "Formula" in schema) must NOT be included in add_records payloads or a 400 error will occur.

Contents

Create a Vendor

add_records("Vendors", [{
    "Name": "Acme Corp",
    "DefaultExpenseAccount": 36,  # Software & Subscriptions
    "PaymentTerms": "Due on Receipt",
    "Notes": "Software vendor",
    "IsActive": True
}])
# Returns: {"inserted_ids": [vendor_id]}

Create Items for Common Purchases

add_records("Items", [{
    "Name": "Monthly Software",
    "DefaultAccount": 36,
    "DefaultDescription": "Monthly SaaS subscription",
    "IsActive": True
}])

Complete Bill Entry (6 Steps)

Step 1: Create Bill Header

add_records("Bills", [{
    "Vendor": 1,  # vendor_id
    "BillNumber": "INV-001",
    "BillDate": 1759708800,  # Unix timestamp
    "DueDate": 1759708800,
    "Status": "Open",
    "Memo": "October services"
}])
# Returns: {"inserted_ids": [bill_id]}

Step 2: Create Bill Line(s)

add_records("BillLines", [{
    "Bill": 1,  # bill_id from step 1
    "Item": 1,  # optional - auto-fills Account/Description
    "Account": 36,  # expense account
    "Description": "Monthly subscription",
    "Amount": 100.00
}])

Step 3: Create Journal Entry

# Transaction header
add_records("Transactions", [{
    "Date": 1759708800,
    "Description": "Acme Corp - October services",
    "Reference": "INV-001",
    "Status": "Posted"
}])
# Returns: {"inserted_ids": [txn_id]}

# Transaction lines: Debit expense, Credit AP
add_records("TransactionLines", [
    {"Transaction": 1, "Account": 36, "Debit": 100.00, "Credit": 0, "Memo": "Monthly subscription"},
    {"Transaction": 1, "Account": 4, "Debit": 0, "Credit": 100.00, "Memo": "Monthly subscription"}
])

Step 4: Link Bill to Transaction

update_records("Bills", [{"id": 1, "fields": {"EntryTransaction": 1}}])

Step 5: Upload Invoice (if available)

bash scripts/upload-attachment.sh invoice.pdf Bills 1 $TOKEN Invoice

Step 6: Post-Entry Audit (REQUIRED)

Run audit checks before concluding. See Audit Reference for details.

-- Check 1: Transaction balanced
SELECT IsBalanced FROM Transactions WHERE id = {txn_id}
-- Expected: true

-- Check 2: Bill integrity
SELECT id, Vendor, EntryTransaction, Amount FROM Bills WHERE id = {bill_id}
-- Expected: All fields populated, Amount > 0

Pay Bill from Checking Account

# Step 1: Create payment transaction
add_records("Transactions", [{
    "Date": 1760832000,
    "Description": "Payment - Acme Corp INV-001",
    "Reference": "Check #1001",
    "Status": "Cleared"
}])
# Returns: {"inserted_ids": [txn_id]}

# Step 2: Debit AP, Credit Checking
add_records("TransactionLines", [
    {"Transaction": 2, "Account": 4, "Debit": 100.00, "Credit": 0, "Memo": "Pay INV-001"},
    {"Transaction": 2, "Account": 14, "Debit": 0, "Credit": 100.00, "Memo": "Pay INV-001"}
])

# Step 3: Create BillPayment record
add_records("BillPayments", [{
    "Bill": 1,
    "Transaction": 2,
    "Amount": 100.00,
    "PaymentDate": 1760832000
}])

# Step 4: Update bill status
update_records("Bills", [{"id": 1, "fields": {"Status": "Paid"}}])

# Step 5: Upload receipt (if available)
# bash scripts/upload-attachment.sh receipt.pdf Bills 1 $TOKEN Receipt

# Step 6: Post-Payment Audit (REQUIRED)
# Verify payment transaction balances and bill status updated correctly

Pay Bill via Owner Reimbursement

When the owner pays a business expense personally:

# Step 1: Create payment transaction
add_records("Transactions", [{
    "Date": 1760832000,
    "Description": "Owner payment - Acme Corp INV-001",
    "Reference": "Owner Reimb",
    "Status": "Posted"
}])

# Step 2: Debit AP, Credit Due to Owner (not Checking)
add_records("TransactionLines", [
    {"Transaction": 2, "Account": 4, "Debit": 100.00, "Credit": 0, "Memo": "Pay INV-001"},
    {"Transaction": 2, "Account": 22, "Debit": 0, "Credit": 100.00, "Memo": "Owner paid"}
])

# Step 3: Create BillPayment record
add_records("BillPayments", [{
    "Bill": 1,
    "Transaction": 2,
    "Amount": 100.00,
    "PaymentDate": 1760832000
}])

# Step 4: Update bill status
update_records("Bills", [{"id": 1, "fields": {"Status": "Paid"}}])

# Step 5: Upload receipt (if available)
# bash scripts/upload-attachment.sh receipt.pdf Bills 1 $TOKEN Receipt

# Step 6: Post-Payment Audit (REQUIRED)

Reimburse Owner

When business pays back the owner:

add_records("Transactions", [{
    "Date": 1762041600,
    "Description": "Owner reimbursement",
    "Reference": "Transfer",
    "Status": "Cleared"
}])

add_records("TransactionLines", [
    {"Transaction": 3, "Account": 22, "Debit": 500.00, "Credit": 0, "Memo": "Reimburse owner"},
    {"Transaction": 3, "Account": 14, "Debit": 0, "Credit": 500.00, "Memo": "Reimburse owner"}
])

Batch Operations

When entering multiple bills efficiently:

  1. Create all Bills first → collect inserted IDs
  2. Create all BillLines referencing bill IDs
  3. Create all Transactions → collect inserted IDs
  4. Create all TransactionLines referencing transaction IDs
  5. Update all Bills with EntryTransaction links in one call
  6. (If paying) Create payment transactions, lines, and BillPayments
  7. Upload invoice attachments if files are available

Batch Attachment Uploads

# Example batch upload pattern
TOKEN=$(request_session_token with write permission)
for each (bill_id, invoice_path):
    curl -X POST -H "Authorization: Bearer $TOKEN" \
         -F "file=@$invoice_path" \
         https://grist-mcp.bballou.com/api/v1/attachments
    # Returns attachment_id
    update_records("Bills", [{"id": bill_id, "fields": {"Invoice": ["L", attachment_id]}}])

Batch Update Example

update_records("Bills", [
    {"id": 1, "fields": {"EntryTransaction": 1}},
    {"id": 2, "fields": {"EntryTransaction": 2}},
    {"id": 3, "fields": {"EntryTransaction": 3}}
])

Sample Payloads

Copy-paste ready JSON payloads for add_records. Only writable fields are included.

Owner Equity Contribution

// Transaction
{"Date": 1768348800, "Description": "Owner equity contribution", "Reference": "Owner Investment", "Status": "Posted", "Memo": ""}

// TransactionLines (Debit Checking, Credit Owner's Investment)
[
  {"Transaction": 51, "Account": 14, "Debit": 1000, "Credit": 0, "Memo": "Owner equity contribution"},
  {"Transaction": 51, "Account": 23, "Debit": 0, "Credit": 1000, "Memo": "Owner equity contribution"}
]

Owner Draw

// Transaction
{"Date": 1768348800, "Description": "Owner draw", "Reference": "Transfer", "Status": "Cleared", "Memo": ""}

// TransactionLines (Debit Owner's Draws, Credit Checking)
[
  {"Transaction": 52, "Account": 24, "Debit": 500, "Credit": 0, "Memo": "Owner draw"},
  {"Transaction": 52, "Account": 14, "Debit": 0, "Credit": 500, "Memo": "Owner draw"}
]

Direct Expense (No Bill)

For bank fees, minor expenses without vendor invoices:

// Transaction
{"Date": 1768348800, "Description": "Bank service fee", "Reference": "Statement", "Status": "Cleared", "Memo": "Monthly account fee"}

// TransactionLines (Debit Expense, Credit Checking)
[
  {"Transaction": 53, "Account": 30, "Debit": 15, "Credit": 0, "Memo": "Monthly fee"},
  {"Transaction": 53, "Account": 14, "Debit": 0, "Credit": 15, "Memo": "Monthly fee"}
]

Revenue Receipt

// Transaction
{"Date": 1768348800, "Description": "Client payment - Project X", "Reference": "INV-100", "Status": "Cleared", "Memo": ""}

// TransactionLines (Debit Checking, Credit Revenue)
[
  {"Transaction": 54, "Account": 14, "Debit": 2500, "Credit": 0, "Memo": "Project X payment"},
  {"Transaction": 54, "Account": 25, "Debit": 0, "Credit": 2500, "Memo": "Service revenue"}
]

Key Account IDs Reference

ID Code Name
4 2000 Accounts Payable
14 1001 Checking Account
22 2203 Due to Owner
23 3001 Owner's Investment
24 3002 Owner's Draws
25 4001 Service Revenue
26 4010 Interest Income
30 5020 Bank & Merchant Fees
36 5080 Software & Subscriptions

Bank Reconciliation

Import bank transactions, match against ledger, create missing entries, and reconcile.

For full reference: see reconciliation.md

Phase 1: Import Bank File (Schwab JSON)

# Read and parse the bank export file
import json
with open("path/to/schwab_export.json") as f:
    data = json.load(f)

# Parse each PostedTransaction
for txn in data["PostedTransactions"]:
    # Date: "MM/DD/YYYY" -> Unix timestamp
    # Amount: parse "$X,XXX.XX" strings; Deposit = positive, Withdrawal = negative
    # Empty string = no amount (skip)
    pass

Phase 2: Match Against Ledger

# Fetch existing TransactionLines for Checking (id=14)
# MUST use get_records, not sql_query (Transaction is reserved word)
get_records("TransactionLines", filter={"Account": [14]})

# Get transaction details for date matching
sql_query("SELECT id, Date, Description, Status FROM Transactions WHERE id IN (...)")

# Match criteria: exact amount AND date ±3 days AND not already matched
# Deposits match Debit > 0 (money into checking = debit to asset)
# Withdrawals match Credit > 0 (money out of checking = credit to asset)

Phase 3: Create Missing Transactions

# For unmatched deposits (e.g., interest income):
add_records("Transactions", [{
    "Date": 1738195200,
    "Description": "Bank interest income",
    "Reference": "Interest Paid",
    "Status": "Cleared",
    "Memo": "Auto-imported from bank statement"
}])
# Returns: {"inserted_ids": [txn_id]}

# Dr Checking, Cr Interest Income
add_records("TransactionLines", [
    {"Transaction": txn_id, "Account": 14, "Debit": 0.01, "Credit": 0, "Memo": "Bank interest"},
    {"Transaction": txn_id, "Account": 26, "Debit": 0, "Credit": 0.01, "Memo": "Bank interest"}
])

# For unmatched withdrawals (e.g., ATM owner draw):
add_records("Transactions", [{
    "Date": 1739750400,
    "Description": "ATM withdrawal - owner draw",
    "Reference": "P421164 88 ESSEX STREET NEW YORK",
    "Status": "Cleared",
    "Memo": "Auto-imported from bank statement"
}])

# Dr Owner's Draws, Cr Checking
add_records("TransactionLines", [
    {"Transaction": txn_id, "Account": 24, "Debit": 102.00, "Credit": 0, "Memo": "ATM withdrawal"},
    {"Transaction": txn_id, "Account": 14, "Debit": 0, "Credit": 102.00, "Memo": "ATM withdrawal"}
])

# Optionally save a BankRule for auto-categorization:
add_records("BankRules", [{
    "Account": 14,
    "Pattern": "Interest Paid",
    "MatchType": "Contains",
    "OffsetAccount": 26,
    "TransactionDescription": "Bank interest income",
    "IsActive": true
}])

Phase 4: Reconcile

# Update matched existing transactions to Cleared status
update_records("Transactions", [
    {"id": existing_txn_id, "fields": {"Status": "Cleared"}}
])

# Calculate cleared balance
lines = get_records("TransactionLines", filter={"Account": [14]})
txn_ids = list(set(l["fields"]["Transaction"] for l in lines))
cleared = sql_query(f"SELECT id FROM Transactions WHERE Status = 'Cleared' AND id IN (...)")
cleared_ids = set(r["id"] for r in cleared)
cleared_balance = sum(
    l["fields"]["Debit"] - l["fields"]["Credit"]
    for l in lines
    if l["fields"]["Transaction"] in cleared_ids
)

# Create Reconciliation record
add_records("Reconciliations", [{
    "Account": 14,
    "StatementDate": 1739750400,  # date of last bank entry
    "StatementBalance": 1398.01,
    "ClearedBalance": cleared_balance,
    "Difference": 1398.01 - cleared_balance,
    "Status": "Completed",  # if Difference == 0
    "StartedAt": 1739923200,  # today
    "CompletedAt": 1739923200,
    "Notes": "Reconciled against Schwab export"
}])

# Verify
sql_query("SELECT * FROM Transactions WHERE IsBalanced = false")