- name
- procesar-mis-estados-de-cuenta
- title
- Procesar mis estados de cuenta
- description
- Proceso un lote de estados de cuenta bancarios y de tarjeta de crédito en PDF o CSV de principio a fin: extraigo cada transacción (subagentes Haiku en paralelo), normalizo los nombres de las contrapartes, categorizo contra tu plan de cuentas bloqueado (subagentes Sonnet en paralelo), detecto transferencias entre cuentas, y armo un libro de Google Sheets revisado con un estado de resultados basado en fórmulas. Las discrepancias de conciliación aparecen como advertencias, las categorizaciones de baja confianza van a Suspenso, nunca invento un código de cuenta, nunca inserto un número en silencio, nunca publico en tu sistema contable.
- version
- 1
- category
- Contabilidad
- featured
- no
- image
- ledger
- integrations
- ["googlesheets","stripe"]
- x_houston
- {"created_by":"houston","skill_schema":1}
# Procesar mis estados de cuenta
Suelta un lote de estados de cuenta bancarios y de tarjeta de crédito en PDF o CSV y produzco un libro de Google Sheets revisado con un estado de resultados basado en fórmulas. Pipeline completo: extraigo cada transacción en paralelo, normalizo las contrapartes, categorizo contra tu plan de cuentas bloqueado, etiqueto las transferencias entre cuentas, y escribo un libro que puedes entregarle a tu contador. El bucket de Suspenso y las advertencias de conciliación quedan arriba de todo, nunca cuadro forzando un número, nunca invento un código de cuenta, nunca publico.
## Destino de la salida: Google Sheets vía Composio
Uso el CLI de Composio disponible en el PATH. Todas las escrituras a Google Sheets pasan por ahí.
**Antes de cualquier ejecución**, verifico que el conjunto de herramientas `googlesheets` esté conectado:
```bash
composio execute GOOGLESHEETS_SEARCH_SPREADSHEETS -d '{"query": "", "max_results": 1}'
```
Si devuelve `"No active connection found for toolkit \"googlesheets\""`, ME DETENGO y te pido que conectes:
```bash
composio link googlesheets --no-wait
```
Tomo `redirect_url` de la respuesta, te la presento como un enlace markdown con `#houston_toolkit=googlesheets` agregado al final (para que Houston muestre la tarjeta de conexión). Espero tu aprobación antes de continuar.
## Conexiones que necesito
Ejecuto el trabajo externo a través de Composio. Antes de correr esta skill, reviso que las categorías de abajo estén vinculadas. Si falta alguna, nombro la categoría, te pido que la conectes desde la pestaña de Integraciones, y me detengo.
- **Google Sheets** (spreadsheets) - obligatorio. Todo el pipeline termina en un libro de Google Sheets con un estado de resultados basado en fórmulas; sin esto no hay resultado. Consulta el bloque "Destino de la salida: Google Sheets vía Composio" más arriba para el comando de verificación y el enlace de conexión.
- **Stripe** (facturación) - opcional. Extrae los depósitos y las comisiones del procesador para que se categoricen limpiamente cuando aparezcan en tu feed bancario.
Si Google Sheets no está conectado, me detengo y te pido que lo conectes antes de hacer cualquier trabajo.
## Información que necesito
Primero leo tu contexto contable. Por cada campo obligatorio que falte, hago UNA pregunta en lenguaje sencillo (mejor modalidad: app conectada > archivo > URL > texto pegado) y espero.
- **Un contexto contable terminado** - Obligatorio. Por qué: necesito tu método contable, tu código de suspenso, y tus cuentas registradas antes de categorizar. Si falta, pregunto: "¿Ya configuramos los libros? Si no, corre la configuración una vez para que yo conozca tu año fiscal, tu método contable, y tus cuentas registradas."
- **Un plan de cuentas** - Obligatorio. Por qué: lo bloqueo durante la ejecución; cada categoría que asigno tiene que venir de tu plan de cuentas. Si falta, pregunto: "¿Ya tenemos un plan de cuentas? Si no, redactemos uno primero."
- **Tus cuentas bancarias y tarjetas de crédito** - Obligatorio. Por qué: agrupo las transacciones por los últimos 4 dígitos y necesito el código de cuenta de cada una. Si falta, pregunto: "¿Qué cuentas bancarias y tarjetas de crédito usa el negocio? Registro automáticamente las nuevas cuando lleguen los estados de cuenta, pero es más rápido si me lo dices de antemano."
- **Los estados de cuenta a procesar** - Obligatorio. Por qué: el pipeline arranca a partir de los PDF o CSV que sueltes. Si falta, pregunto: "¿Puedes soltar los estados de cuenta bancarios y de tarjeta de crédito en PDF, o adjuntarlos en el chat?"
- **Reglas de proveedor de un período anterior** - Opcional. Por qué: me permite emparejar cargos nuevos con proveedores conocidos y mantener al mínimo las preguntas. Si no las tienes, sigo adelante y aprendo de esta ejecución.
## Estructura de almacenamiento
Agente de una sola empresa. El plan de cuentas y la memoria viven en la raíz del agente (plano). Cada ejecución obtiene su propia carpeta bajo `runs/{period}/`.
```
context/
└── bookkeeping-context.md # brief en vivo (entidad, año fiscal, método contable)
config/
├── context-ledger.json # metadatos: empresa, método contable, bancos, etc.
├── chart-of-accounts.json # plan de cuentas autoritativo (bloqueado durante una ejecución)
├── prior-categorizations.json # {canonical_party: gl_code} - historial de proveedores
└── party-rules.json # reglas exactas confirmadas por el usuario
statements/ # PDFs fuente + archivos auxiliares (lista de proveedores, etc.)
└── _inbox/ # zona de entrega para PDFs antes de correr este pipeline
runs/
└── {period}/ # ej., 2024, 2024-Q1, 2024-01
├── run.json # artefacto completo de la ejecución (la fuente de recuperación)
├── _extractions/{pdf_stem}.json # transitorio - resultados del extractor Haiku (uno por PDF)
├── _work/{account_last4}.json # transitorio - paquetes entregados a cada Categorizador
├── _categorizations/{account_last4}.json # transitorio - resultados del categorizador Sonnet
└── _sheet_state/{period}.json # transitorio - resultado del Escritor de Sheets Sonnet
```
Si falta el directorio, lo creo con `mkdir -p` en el primer uso.
**Las cuentas bancarias** viven en el libro de contexto, no en un `client.json` separado:
```jsonc
// config/context-ledger.json (extracto)
{
"domains": {
"banks": {
"accounts": [
{"last4": "9041", "type": "credit-card", "bank": "Chase",
"glCode": "20000", "glName": "Chase CC #9041"}
]
}
},
"universal": {
"suspenseCode": { "code": "99999", "name": "Suspense" }
}
}
```
## Entradas
Tú me das uno o más de estos:
1. Rutas explícitas de PDF en el mensaje (lo más común, adjuntos soltados en el chat).
2. PDFs en `statements/_inbox/`, los listo con `ls statements/_inbox/*.pdf`.
3. Identificador de período (año / trimestre / mes), usado para el nombre de la carpeta `runs/{period}/`.
4. (Opcional) archivo de plan de cuentas personalizado (xlsx / csv / texto en línea), lista de proveedores, o Detalle de Transacciones anterior.
## Procedimiento
<!-- houston-workflow:v1 -->
### Paso 1 - Arranco el contexto y bloqueo el plan de cuentas
1. **Cargo el estado existente:**
- `context/bookkeeping-context.md`, el brief. Si falta, me detengo y pido que corras `set-up-my-books` primero (o que lo hagas en línea).
- `config/context-ledger.json`, cuentas, código de suspenso.
- `config/chart-of-accounts.json`, el plan de cuentas autoritativo. Si existe, lo **BLOQUEO para esta ejecución.**
- `config/prior-categorizations.json`, memoria de proveedor → código de cuenta.
- `config/party-rules.json`, reglas de coincidencia exacta.
2. **Arranque de primera ejecución (solo si `config/chart-of-accounts.json` no existe):**
- Si me diste un archivo de plan de cuentas (xlsx/csv), lo interpreto (openpyxl para xlsx) hacia `config/chart-of-accounts.json` como `[{code, name, type, statementSection}]`.
- Si describiste el plan de cuentas en línea, lo estructuro de esa forma.
- Si no, recurro al que viene por defecto en `CHART_OF_ACCOUNTS.md`, pero lo copio a `config/chart-of-accounts.json` para que las siguientes ejecuciones compartan los mismos códigos.
- Copio los PDFs fuente + archivos auxiliares a `statements/` (mantengo los nombres de archivo; subcarpetas por cuenta está bien, ej. `statements/9041/2024-01.pdf`).
- Si me diste un Detalle de Transacciones anterior, extraigo `{vendor_name: [gl_codes]}` y siembro `config/prior-categorizations.json` con el código mayoritario por proveedor (solo si es consistente en ≥ 80% de los registros anteriores).
3. **Bloqueo el plan de cuentas para el resto de la ejecución.** Trato `config/chart-of-accounts.json` como inmutable hasta el Paso 7. Si una transacción no se puede categorizar, la mando a Suspenso, NUNCA invento un código de cuenta nuevo.
4. **Determino el período.** Por defecto: desde el mínimo (period_start) hasta el máximo (period_end) entre todos los estados de cuenta. Slug de período: `YYYY` para año completo, `YYYY-QN` para trimestre, `YYYY-MM` para un solo mes. Creo `runs/{period}/_extractions/`, `runs/{period}/_work/`, `runs/{period}/_categorizations/`, `runs/{period}/_sheet_state/`.
### Paso 2 - Extraigo las transacciones (subagentes Haiku en paralelo)
**No leo los PDFs en el orquestador, despacho subagentes Haiku en paralelo.** Es mucho más rápido, y mantiene limpio el contexto del orquestador para la categorización y el armado de la hoja de cálculo.
**Patrón de despacho:**
Por cada PDF (o lote pequeño de ≤ 3 PDFs de un solo mes de la misma cuenta), lanzo una llamada `Agent` en paralelo con:
- `subagent_type: "general-purpose"`
- `model: "haiku"`
- `description: "Extract {bank} {account_last4} {YYYY-MM}"` (o similar, de 3 a 5 palabras)
**Envío todos los despachos en un solo mensaje para que corran de forma concurrente.** Doce estados de cuenta mensuales → doce agentes en paralelo, terminan en aproximadamente el tiempo de uno solo.
Cada subagente escribe su resultado en disco en `runs/{period}/_extractions/{source_pdf_stem}.json` y devuelve una confirmación corta ("wrote N transactions, reconciles: yes/no"). El orquestador lee de vuelta los archivos JSON después de que todos los agentes terminan.
**Plantilla del prompt del subagente** (pega, completa `{...}` por cada despacho):
```
You are extracting transactions from a single bank or credit card statement PDF.
PDF path: {absolute_pdf_path}
Expected account_last4 (if known): {last4 or "unknown"}
Expected account type: {"credit_card" | "checking" | "savings" | "unknown"}
TASK
Read the PDF with the Read tool (it is multimodal - it sees the pages). If the PDF has
more than 10 pages, use the `pages` parameter to read it in slices. Extract EVERY
transaction and the statement's opening/closing balances. Write the result as JSON to:
{output_path}
OUTPUT JSON SCHEMA
{
"source_pdf": "{pdf filename, not path}",
"bank_name": "Chase" | "Wells Fargo" | etc.,
"account_last4": "9041",
"account_type": "credit_card" | "checking" | "savings",
"statements": [ // generalmente uno, pero los PDF multiperíodo pueden tener varios
{
"statement_date": "2023-01-12",
"period_start": "2022-12-13",
"period_end": "2023-01-12",
"opening_balance": 1090.96,
"closing_balance": 1085.63,
"transactions": [
{"date":"2022-12-15","description":"...","amount":-45.00,"source_page":3}
]
}
]
}
SIGN CONVENTION - NON-NEGOTIABLE
Normalize to "money out of the business = negative, money in = positive":
- Checking / savings: deposits +, withdrawals / debits / fees -.
- Credit card: purchases / interest / fees -, payments / credits / returns +.
(This is the OPPOSITE of how many CC statements print; flip if needed.)
EXTRACTION DISCIPLINE
- The transaction AMOUNT is the change, not the running balance column.
- Skip "Beginning Balance" and "Ending Balance" marker rows.
- Include bank fees and interest as transactions.
- Continued-on-next-page rows: include once.
- Multi-period PDFs: emit one entry per statement under `statements[]`.
- Date format: ISO YYYY-MM-DD. If a txn date is ambiguous (12/15 with no year) use the
year consistent with the statement period.
RECONCILIATION SELF-CHECK
Before writing the file, verify for each statement:
computed_close = opening_balance + sum(transaction.amount) (for checking/savings)
computed_close = opening_balance - sum(transaction.amount) (for credit_card, using the sign convention above)
If |computed_close - closing_balance| > 0.02, include a "reconciliation_note" field
on that statement describing the diff - do NOT silently force a match.
Write the JSON file. Return a one-line summary:
"wrote {N} txns across {M} statement(s), recon: {ok|diff=$X.XX}"
```
**Después de despachar, el orquestador:**
1. Espero a que todos los subagentes terminen (corren en paralelo automáticamente).
2. Leo cada `runs/{period}/_extractions/*.json`.
3. Combino en una sola lista en memoria por account_last4.
4. Elimino duplicados en `(account_last4, date, amount, description)` si dos estados de cuenta se traslapan.
5. Aplico la misma autoverificación de conciliación en el orquestador (confío, pero verifico).
**Cuándo NO despachar subagentes:**
- Solo un PDF pequeño, necesito los datos de inmediato, lo leo en línea.
- PDF escaneado como imagen, de muy baja calidad, lo hago yo mismo para poder inspeccionar visualmente los artefactos del OCR.
- El subagente devolvió una diferencia de conciliación > $0.02, releo yo mismo ese estado de cuenta específico en el orquestador y corrijo la extracción.
Consulta `EXTRACTION.md` para los patrones de formato nombrados (tablas simples, columnas de saldo corrido, el formato en español de Wells Fargo, etc.), incluyo la pista de patrón relevante en el prompt del subagente cuando conozco el banco de antemano.
### Paso 3 - Verificación de conciliación (solo advertencia, nunca bloquea)
Por cada estado de cuenta:
```
computed_closing = opening_balance + sum(transaction.amount for transaction in statement)
mismatch = abs(computed_closing - closing_balance) > 0.02 # tolerancia de 2 centavos
```
Si hay descuadre, agrego una advertencia a la hoja de conciliación y continúo. No detengo el pipeline.
### Paso 3b - Combino las extracciones y escribo los paquetes de trabajo para el Categorizador
Después de que todos los Extractores Haiku terminan, el orquestador lee y combina el resultado antes de despachar a los Categorizadores:
1. **Leo todos los `runs/{period}/_extractions/*.json`.**
2. **Agrupo las transacciones por `account_last4`.** Por cada last4 único entre todos los archivos de extracción, recolecto todas las transacciones de todos los estados de cuenta de esa cuenta.
3. **Registro cuentas nuevas.** Cualquier `account_last4` que aún no esté en `context-ledger.json → domains.banks.accounts[]`, la agrego con el nombre del banco y el tipo de cuenta del archivo de extracción, dejo `gl_code` en blanco por ahora.
4. **Elimino duplicados.** Dentro de cada cuenta, quito las transacciones duplicadas en `(date, amount, description)`, aparecen cuando los estados de cuenta se traslapan (ej., dos meses comparten una fecha límite).
5. **Escribo un paquete de trabajo por cuenta** en `runs/{period}/_work/{account_last4}.json`:
```json
{
"account_last4": "9041",
"account_type": "credit_card",
"bank": "Chase",
"gl_code": "20000",
"suspense_code": "99999",
"transactions": [
{ "date": "2023-01-15", "description": "AMAZON.COM*AB12C NJ", "amount": -45.00, "statement_date": "2023-01-20" }
],
"chart_of_accounts": [
{ "code": "6090", "name": "Office Expenses", "type": "expense" }
],
"prior_categorizations": { "Amazon": "6090" },
"party_rules": { "PG&E": "6150" }
}
```
Campos:
- `account_last4`, `account_type`, `bank`, `gl_code`, de `context-ledger.json → domains.banks.accounts[]` (`gl_code` puede quedar en blanco para cuentas nuevas)
- `suspense_code`, de `context-ledger.json → universal.suspenseCode.code`
- `transactions`, lista combinada y sin duplicados solo de esta cuenta; incluye `statement_date` si está presente en el JSON de extracción
- `chart_of_accounts`, el contenido completo de `config/chart-of-accounts.json`
- `prior_categorizations`, el contenido completo de `config/prior-categorizations.json` (`{}` vacío si está ausente)
- `party_rules`, el contenido completo de `config/party-rules.json` (`{}` vacío si está ausente)
6. **Creo los subdirectorios de salida si no existen:**
```bash
mkdir -p runs/{period}/_work
mkdir -p runs/{period}/_categorizations
mkdir -p runs/{period}/_sheet_state
```
### Paso 4+5 - Despacho a los subagentes Categorizadores (Sonnet, en paralelo)
**No canonicalizo ni categorizo en línea en el orquestador.** Despacho un Categorizador Sonnet por cada `account_last4` en un solo mensaje para que corran de forma concurrente. Para cuentas con más de 500 transacciones, divido en bloques de ≤500 filas y despacho varios agentes para la misma cuenta (los resultados se concatenan en orden).
**Patrón de despacho:**
Por cada cuenta (una llamada `Agent` por cuenta en un solo mensaje):
- `subagent_type: "general-purpose"`
- `model: "sonnet"`
- `description: "Categorize {bank} {account_last4}"` (de 3 a 5 palabras)
**Cada Categorizador devuelve un estado de una línea:**
`"account {last4}: {N} txns, {R} ready / {V} review / {U} suspense (${S})"`
---
**Plantilla del prompt del subagente Categorizador** (completa `{...}` por cada cuenta):
```
You are categorizing bank/credit card transactions for bookkeeping.
Work packet path: {absolute_work_packet_path}
Output path: {absolute_output_path}
TASK
1. Read the work packet JSON at the work packet path above.
2. For each transaction, canonicalize the party name (Stage 4 below) then categorize it (Stage 5 below).
3. Write the result JSON to the output path.
Ver no GitHub