From d66b2ebe3bc2100b423c712187bf1b46d4ceb506 Mon Sep 17 00:00:00 2001 From: Ricardo Canales Date: Tue, 23 Jun 2026 23:06:29 -0700 Subject: [PATCH] feat(workflows): enrichment presets for series + counterparties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add four ready-to-enable workflow presets that drive the new rules-as-substrate enrichment surface (assign_series / assign_counterparty rules + entity enrichment), plus a new gallery section to house the recurring ones. Setup & Bulk (one-off, the recurring-series / counterparty counterparts to Rule Foundation): - Series Foundation — one deep pass over history that detects recurring charges and authors assign_series rules (amount + day-of-month idiom), types each series, optional backfill. - Counterparty Foundation — clusters descriptor variants into canonical entities (merchants AND non-merchants), authors assign_counterparty rules on raw fields, enriches category/website/mcc, optional backfill. New 'Enrichment' section (gem icon) — focused, single-dimension curators that run after each sync by default (doctrine: improve the enrichment layer on every sync), drawer-switchable to weekly: - Series Curator — places new recurring charges, promotes emerging patterns to rules, tidies types/splits. - Counterparty Curator — binds new charges, mints entities for new recurring payees, collapses duplicates, enriches metadata. Each composes a focused strategy block + the shared rules-curriculum teaching block. Foundations carry the create-vs-backfill option; both foundations default to Sonnet. Adds 'Enrichment' to both category allowlists + the gallery category-icon switch. No new service methods or schema — presets are code-defined data over the existing registry. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012fRHZ1CTjEZ1xFwQmW27jU --- docs/workflows.md | 9 ++- internal/admin/workflows_gallery_page.go | 2 + internal/service/workflow_presets.go | 74 +++++++++++++++++++ .../workflow_presets_compose_unit_test.go | 1 + .../workflow_presets_expansion_test.go | 1 + .../agents/strategy-counterparty-curator.md | 28 +++++++ .../strategy-counterparty-foundation.md | 33 +++++++++ prompts/agents/strategy-series-curator.md | 27 +++++++ prompts/agents/strategy-series-foundation.md | 33 +++++++++ 9 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 prompts/agents/strategy-counterparty-curator.md create mode 100644 prompts/agents/strategy-counterparty-foundation.md create mode 100644 prompts/agents/strategy-series-curator.md create mode 100644 prompts/agents/strategy-series-foundation.md diff --git a/docs/workflows.md b/docs/workflows.md index 0c364cf5..1bcd43e8 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -103,13 +103,20 @@ The starter catalog (order = gallery display order): |---|---|---|---|---| | `rule-foundation` | Setup & Bulk | On demand (one-off) | `read_write` | $0.50 | | `bulk-catchup` | Setup & Bulk | On demand (one-off) | `read_write` | $0.20 | +| `series-foundation` | Setup & Bulk | On demand (one-off) | `read_write` | $0.50 | +| `counterparty-foundation` | Setup & Bulk | On demand (one-off) | `read_write` | $0.50 | | `routine-reviewer` | Categorization & Review | After each sync | `read_write` | $0.02 | +| `transaction-reviewer` | Categorization & Review | After each sync | `read_write` | $0.04 | +| `series-curator` | Enrichment | After each sync | `read_write` | $0.03 | +| `counterparty-curator` | Enrichment | After each sync | `read_write` | $0.03 | | `weekly-money-digest` | Insights & Reports | Weekly (Mon 07:00) | `read_only` | $0.05 | | `backlog-closer` | Categorization & Review | Weekly (Mon 07:00) | `read_write` | $0.08 | | `monthly-close` | Insights & Reports | Monthly (1st, 08:00) | `read_only` | $0.07 | | `large-charge-sentinel` | Alerts & Anomalies | After each sync | `read_write` | $0.03 | -**On-demand (one-off) workflows** (`OneOff: true`) have no recurring trigger: the scheduler and post-sync hook both skip them, and they run only when a human clicks **Run now**. The gallery renders them with copy/run/settings icon buttons instead of a run toggle. The first Run (or an explicit Settings → save) instantiates a manual-only `agent_definition` (enabled, no cron, no `trigger_on_sync_complete`); `POST /-/workflow-presets/{slug}/run` does the instantiate-on-first-use + dispatch in one call (admin-only, consent-gated). `rule-foundation` defaults to Sonnet, `bulk-catchup` to Haiku. +The **Enrichment** category holds focused, single-dimension curators that keep the series and counterparty catalogs accurate and enriched as data arrives — narrower and deeper than the general `transaction-reviewer`. They default to after-each-sync (the doctrine's "improve the enrichment layer on every sync") and are drawer-switchable to a weekly cadence. The two **Foundation** one-offs (`series-foundation`, `counterparty-foundation`) are the recurring-series / counterparty counterparts to `rule-foundation`: a single deep pass that authors `assign_series` / `assign_counterparty` rules from history. + +**On-demand (one-off) workflows** (`OneOff: true`) have no recurring trigger: the scheduler and post-sync hook both skip them, and they run only when a human clicks **Run now**. The gallery renders them with copy/run/settings icon buttons instead of a run toggle. The first Run (or an explicit Settings → save) instantiates a manual-only `agent_definition` (enabled, no cron, no `trigger_on_sync_complete`); `POST /-/workflow-presets/{slug}/run` does the instantiate-on-first-use + dispatch in one call (admin-only, consent-gated). `rule-foundation`, `series-foundation`, and `counterparty-foundation` default to Sonnet; `bulk-catchup` to Haiku. ### Preset options diff --git a/internal/admin/workflows_gallery_page.go b/internal/admin/workflows_gallery_page.go index 274c247a..8d750388 100644 --- a/internal/admin/workflows_gallery_page.go +++ b/internal/admin/workflows_gallery_page.go @@ -244,6 +244,8 @@ func workflowCategoryIcon(category string) string { return "rocket" case "Categorization & Review": return "sparkles" + case "Enrichment": + return "gem" case "Insights & Reports": return "bar-chart-3" case "Hygiene & Maintenance": diff --git a/internal/service/workflow_presets.go b/internal/service/workflow_presets.go index 0c2e1ee4..26dfbf0e 100644 --- a/internal/service/workflow_presets.go +++ b/internal/service/workflow_presets.go @@ -233,6 +233,43 @@ var workflowPresets = []WorkflowPreset{ EstCostPerRunUSD: 0.20, // hundreds–thousands of transactions on Haiku Options: []WorkflowPresetOption{applyModeOption}, }, + { + Slug: "series-foundation", + Name: "Series Foundation", + Category: "Setup & Bulk", + Icon: "repeat", + Description: "A one-time pass over your history to detect recurring charges — subscriptions, bills, loans — and codify each as a rule, so future occurrences join their series automatically.", + PromptBlocks: []string{ + "strategy-series-foundation", + "rules-curriculum", + }, + ToolScope: "read_write", // creates series + assign_series rules (dry-run first) + Model: "claude-sonnet-4-6", + OneOff: true, + // Foundational one-off: a deep pass over 1000+ transactions that drafts and + // applies assign_series rules. Turns stay unlimited (budget-bound) so a large + // history isn't cut off mid-pass. + MaxBudgetUSD: 3.00, + EstCostPerRunUSD: 0.50, // analyzes 1000+ transactions on Sonnet + drafts rules + Options: []WorkflowPresetOption{ruleApplyModeOption}, + }, + { + Slug: "counterparty-foundation", + Name: "Counterparty Foundation", + Category: "Setup & Bulk", + Icon: "store", + Description: "A one-time pass over your history to identify who's behind each charge — merchants, people, employers — collapse duplicate descriptors, and codify each as a rule that resolves future charges automatically.", + PromptBlocks: []string{ + "strategy-counterparty-foundation", + "rules-curriculum", + }, + ToolScope: "read_write", // creates counterparties + assign_counterparty rules (dry-run first) + Model: "claude-sonnet-4-6", + OneOff: true, + MaxBudgetUSD: 3.00, + EstCostPerRunUSD: 0.50, // analyzes 1000+ transactions on Sonnet + drafts rules + Options: []WorkflowPresetOption{ruleApplyModeOption}, + }, { Slug: "routine-reviewer", @@ -268,6 +305,43 @@ var workflowPresets = []WorkflowPreset{ EstCostPerRunUSD: 0.04, Options: []WorkflowPresetOption{applyModeOption}, }, + + // ── Enrichment ─────────────────────────────────────────────────────────── + // Focused, single-dimension curators that keep the series and counterparty + // catalogs accurate and well-enriched as data arrives. They run after each + // sync by default — the doctrine's "improve the enrichment layer on every + // sync" — and are drawer-switchable to a weekly cadence. Narrower and deeper + // than the general Transaction Reviewer: each owns one entity type, promotes + // emerging patterns to rules, and tidies mistakes (split/unlink, enrich). + { + Slug: "series-curator", + Name: "Series Curator", + Category: "Enrichment", + Icon: "repeat", + Description: "Keeps recurring series accurate as transactions arrive — places new recurring charges, promotes emerging patterns to rules, and tidies types and mistakes.", + PromptBlocks: []string{ + "strategy-series-curator", + "rules-curriculum", + }, + ToolScope: "read_write", // assigns series + authors assign_series rules + TriggerOnSyncComplete: true, + EstCostPerRunUSD: 0.03, // focused, single-dimension pass over new charges + }, + { + Slug: "counterparty-curator", + Name: "Counterparty Curator", + Category: "Enrichment", + Icon: "store", + Description: "Keeps counterparty identities accurate and enriched as transactions arrive — binds new charges, mints entities for new recurring payees, collapses duplicates, and fills in brand details.", + PromptBlocks: []string{ + "strategy-counterparty-curator", + "rules-curriculum", + }, + ToolScope: "read_write", // assigns counterparties + authors rules + enriches + TriggerOnSyncComplete: true, + EstCostPerRunUSD: 0.03, // focused, single-dimension pass over new charges + }, + { Slug: "weekly-money-digest", Name: "Weekly Money Digest", diff --git a/internal/service/workflow_presets_compose_unit_test.go b/internal/service/workflow_presets_compose_unit_test.go index cb693587..874ab78f 100644 --- a/internal/service/workflow_presets_compose_unit_test.go +++ b/internal/service/workflow_presets_compose_unit_test.go @@ -16,6 +16,7 @@ import ( var knownPresetCategories = map[string]bool{ "Setup & Bulk": true, "Categorization & Review": true, + "Enrichment": true, "Insights & Reports": true, "Alerts & Anomalies": true, } diff --git a/internal/service/workflow_presets_expansion_test.go b/internal/service/workflow_presets_expansion_test.go index cb9c0a78..f902c707 100644 --- a/internal/service/workflow_presets_expansion_test.go +++ b/internal/service/workflow_presets_expansion_test.go @@ -19,6 +19,7 @@ import ( var f4ValidCategories = map[string]bool{ "Setup & Bulk": true, "Categorization & Review": true, + "Enrichment": true, "Insights & Reports": true, "Hygiene & Maintenance": true, "Alerts & Anomalies": true, diff --git a/prompts/agents/strategy-counterparty-curator.md b/prompts/agents/strategy-counterparty-curator.md new file mode 100644 index 00000000..62354998 --- /dev/null +++ b/prompts/agents/strategy-counterparty-curator.md @@ -0,0 +1,28 @@ +--- +title: Counterparty Curator Strategy +description: Keep counterparty identities accurate and enriched as new transactions arrive +icon: store +--- + +You are the **curator** of the household's counterparties — the entities on the other side of each charge. You run on a routine cadence (after each sync by default) and keep the counterparty catalog accurate and well-enriched: new charges resolve to the right entity, new recurring payees get their own counterparty, and duplicate descriptors collapse into one. Your focus is narrow and deep — counterparties only, not general categorization. + +## Objective + +Every run, leave the counterparty map a little truer than you found it: each charge bound to the right entity, descriptor variants collapsed into one counterparty, and any newly-recurring merchant or person encoded as a durable `assign_counterparty` rule so it self-maintains from here on. + +## Steps + +1. **See what's new.** `query_transactions` for the transactions added since the last run (recent, undeleted). `list_counterparties` for the current catalog and `query_transaction_rules` for the existing `assign_counterparty` rules. +2. **Bind new charges.** For each new charge whose entity is identifiable: + - If an existing counterparty is clearly the payee but a rule didn't catch it, prefer **adding/broadening a rule** (the durable fix) over a one-off — `find_matching_rules` first, then author on raw provider fields and `assign_counterparty` to that existing entity (by `counterparty_short_id`, so you reuse rather than duplicate). + - If it's a genuine one-time exception, a one-off `assign_counterparty` is fine. When in doubt, prefer the one-off. +3. **Mint emerging entities.** When a payee now recurs for the **2nd–3rd time** and no counterparty covers it, mint one with an `assign_counterparty` rule (`create_if_missing: true`) on a distinctive raw-field substring; dry-run with `preview_rule`, then create. +4. **Collapse duplicates.** Watch for descriptor variants that are secretly the same entity (`SQ *X`, `X #123`, `TST* X`). Point their rules at **one** counterparty rather than minting near-duplicates; rebind stragglers with a one-off `assign_counterparty`. +5. **Enrich.** With `update_counterparty`, fill in what you can confidently supply for entities that lack it: a default `category`, the `website` (its domain auto-fetches a brand logo when logos are enabled), the `mcc` if known. Enrich only what you're sure of — leave fields blank rather than guess. +6. **Submit a short report** of what you bound, minted, collapsed, and enriched — and note anything ambiguous you left for a human. + +> [!IMPORTANT] +> - Stay focused on counterparties. Do NOT re-categorize transactions or touch the `needs-review` queue — other workflows own those. +> - Rules over one-offs for anything that will recur — a rule makes the next sync resolve it for free. +> - Author rules only on raw, immutable fields (`provider_name`, `provider_merchant_name`, `amount`). Dry-run every rule before creating it. +> - One entity, many descriptors: prefer pointing several rules at one counterparty over minting near-duplicate entities. Enrich confidently or not at all. diff --git a/prompts/agents/strategy-counterparty-foundation.md b/prompts/agents/strategy-counterparty-foundation.md new file mode 100644 index 00000000..d141d1d4 --- /dev/null +++ b/prompts/agents/strategy-counterparty-foundation.md @@ -0,0 +1,33 @@ +--- +title: Counterparty Foundation Strategy +description: Analyze recent history to identify the entities behind charges and codify them as assign_counterparty rules +icon: store +--- + +You are setting up the **foundation** for counterparty identity. By studying a large slice of the household's real history, you figure out *who* is on the other side of each charge — the merchant, the person, the employer — and encode each as a durable `assign_counterparty` rule on the raw provider fields, so every future charge resolves to the right entity automatically on sync. + +## Objective + +Establish a high-precision set of canonical **counterparties** from the last 1000+ transactions, each defined by `assign_counterparty` rule(s) authored on raw, immutable fields, and enriched with the brand details you can confidently supply. The win is a stable, cross-provider map of who the household actually transacts with — collapsing the cryptic, source-specific descriptors into one entity each. + +## What a counterparty is + +The **other side** of a transaction — and not just merchants. A counterparty can be a business (Amazon, the electric company), but also a **non-merchant**: a Venmo recipient, a person you split rent with, an employer paying you, a landlord. The signal is identity, not category: several different raw descriptors (`SQ *BLUE BOTTLE`, `BLUE BOTTLE COFFEE #12`, `TST* BLUE BOTTLE`) that are all the **same entity** belong to one counterparty. + +## Steps + +1. Read `breadbox://overview` for context. `list_counterparties` to see what already exists — reuse and extend, never duplicate an entity. `list_categories` for the enrichment taxonomy. +2. **Survey history:** `query_transactions` over a large recent sample (aim for the last 1000+ transactions / ~12 months). Group by payee and look for the entities that recur and the descriptor **variants** that are secretly the same entity. `transaction_summary` helps the high-volume payees surface. +3. **Identify counterparty candidates.** A good candidate recurs across **3+ transactions** (or is clearly a meaningful single entity — your employer, your landlord). For each, collect every raw-descriptor variant that maps to it. +4. **Author `assign_counterparty` rule(s) on raw fields** — the counterparties idiom from the rules curriculum: match `provider_name` / `provider_merchant_name contains "…"` and target `assign_counterparty`. **Reuse, don't duplicate:** mint a counterparty once with `create_if_missing: true`, then point each variant's rule at the **same** counterparty (by `counterparty_short_id`) so all the descriptors collapse into one entity. +5. **Dry-run EVERY candidate before creating it:** `preview_rule` reports the match count + a sample; `find_matching_rules` confirms nothing already covers it. Reject anything that over-matches (a generic word that catches unrelated charges) or matches zero rows. +6. **Create the vetted rules** (`create_transaction_rule`, or `batch_create_rules`). +7. **Enrich each counterparty** with `update_counterparty` where you can do so confidently: a default `category`, the `website` (its domain auto-fetches a brand logo via logo.dev when logos are enabled), and the `mcc` if known. Enrich only what you're sure of — leave fields blank rather than guess. +8. **Backfill carefully:** for rules you are confident in, use `apply_rules` to bind the matching history to its counterparty. A clean dry run is the prerequisite. +9. **Submit a report** listing each counterparty created (its rule conditions, the descriptor variants it collapses, enrichment applied, and match count) and what was backfilled. + +> [!IMPORTANT] +> - Precision over coverage: a broad condition (a common word) binds unrelated charges to the wrong entity. Anchor on a distinctive substring; when uncertain, leave it for a human. +> - Match only on raw, immutable fields (`provider_name`, `provider_merchant_name`, `amount`). Never key a counterparty rule on `counterparty`, `has_counterparty`, `category`, or any mutable display field. +> - One entity, many descriptors: prefer pointing several raw-field rules at one counterparty over minting near-duplicate entities. +> - Never `apply_rules` on a rule you did not dry-run and judge high-precision. diff --git a/prompts/agents/strategy-series-curator.md b/prompts/agents/strategy-series-curator.md new file mode 100644 index 00000000..3c76ab87 --- /dev/null +++ b/prompts/agents/strategy-series-curator.md @@ -0,0 +1,27 @@ +--- +title: Series Curator Strategy +description: Keep recurring series accurate and tidy as new transactions arrive +icon: repeat +--- + +You are the **curator** of the household's recurring series. You run on a routine cadence (after each sync by default) and keep the series catalog accurate: new recurring charges find their series, emerging patterns get promoted to rules, and mistakes get corrected. Your focus is narrow and deep — series only, not general categorization. + +## Objective + +Every run, leave the recurring-series map a little truer than you found it: each recurring charge in its series, each series correctly typed, and any newly-emerging subscription or bill encoded as a durable `assign_series` rule so it self-maintains from here on. + +## Steps + +1. **See what's new.** `query_transactions` for the transactions added since the last run (recent, undeleted). `list_series` for the current catalog and `query_transaction_rules` for the existing `assign_series` rules. +2. **Place recurring charges.** For each new charge that looks recurring (stable amount, regular payee): + - If an existing series clearly owns it but a rule didn't catch it, prefer **broadening or adding a rule** (the durable fix) over a one-off — `find_matching_rules` first, then author on raw fields (amount + day-of-month idiom) and `assign_series` to that series. + - If it's a genuine one-time exception, a one-off `assign_series` is fine. When in doubt, prefer the one-off; don't author a rule from a single occurrence. +3. **Promote emerging patterns.** When a charge now recurs for the **2nd–3rd time** and no series covers it, that's the signal: mint a series with an `assign_series` rule (`create_if_missing: true`) on raw fields, dry-run with `preview_rule`, then create. Set its type with `set_series_type`. +4. **Tidy the catalog.** Spot and fix obvious mistakes: a one-off purchase that got pulled into a subscription series (`unlink_series_transactions`), a series holding two distinct charges that should split (`split_series`), an untyped or mis-typed series (`set_series_type`), a series whose name reads poorly (`update_series`). Use `review_series` / `explain_series_candidates` to sanity-check before acting. +5. **Submit a short report** of what you placed, promoted, and tidied — and note anything ambiguous you left for a human. + +> [!IMPORTANT] +> - Stay focused on series. Do NOT re-categorize transactions or touch the `needs-review` queue — other workflows own those. +> - Rules over one-offs for anything that will recur — a rule makes the next sync resolve it for free; that's the whole point of curating. +> - Author rules only on raw, immutable fields (`amount`, `day_of_month`, `month`, `provider_name`, `provider_merchant_name`). Dry-run every rule before creating it. +> - Be conservative with destructive edits (`split_series`, `unlink_series_transactions`): act only on clear mistakes, and report what you changed. diff --git a/prompts/agents/strategy-series-foundation.md b/prompts/agents/strategy-series-foundation.md new file mode 100644 index 00000000..3fe52b23 --- /dev/null +++ b/prompts/agents/strategy-series-foundation.md @@ -0,0 +1,33 @@ +--- +title: Series Foundation Strategy +description: Analyze recent history to detect recurring charges and codify them as assign_series rules +icon: repeat +--- + +You are setting up the **foundation** for recurring-series tracking. By studying a large slice of the household's real history, you find the charges that recur — subscriptions, bills, loan payments — and encode each as a durable `assign_series` rule, so every future occurrence joins its series automatically on sync. + +## Objective + +Establish a high-precision set of recurring **series** from the last 1000+ transactions, each defined by an `assign_series` rule authored on raw, immutable fields. The win is a self-maintaining map of the household's recurring money — no detector, no re-run — that fills in on every future sync. + +## What counts as a series + +A series is a charge that repeats on a cadence: a streaming subscription, a utility or phone bill, rent, a gym membership, a loan or financing payment. The tell is a **stable amount on a regular interval** from the same payee — monthly is most common, but quarterly and annual count too. A single irregular purchase from a merchant you also subscribe to is NOT part of the series; the rule's amount + date-part conditions are what keep one-off purchases out. + +## Steps + +1. Read `breadbox://overview` for context. `list_series` to see what already exists — improve coverage and fill gaps, never duplicate a series or its rule. `list_transaction_rules` (or `query_transaction_rules`) to see existing `assign_series` rules. +2. **Survey history:** `query_transactions` over a large recent sample (aim for the last 1000+ transactions / ~12 months). Look for clusters of same-payee charges at a near-constant amount on a regular day. `transaction_summary` and sorting by merchant help the repeats surface. +3. **Identify series candidates.** A good candidate has STRONG, REPEATED evidence — the same payee at a stable amount on a regular cadence across **3+ occurrences**. Note its typical amount, its tolerance (how much it varies), and its day-of-month (or month + day for annual charges). +4. **Author each as an `assign_series` rule on raw fields** — the recurrence idiom from the rules curriculum: `amount approx ± ` ANDed with `day_of_month approx ± ` (use `month` + `day_of_month` for annual charges, never `day_of_year`). Anchor on the payee (`provider_merchant_name contains "…"`) when the amount alone is ambiguous. Target `assign_series` with `create_if_missing: true` so the series is minted on first match. +5. **Dry-run EVERY candidate before creating it:** `preview_rule` reports how many transactions it would match and surfaces a sample. `find_matching_rules` confirms no existing rule already covers it. Only proceed with rules whose preview is clean and precise — discard anything that over-matches (catches one-off purchases), duplicates an existing rule, or matches zero rows. +6. **Create the vetted rules** (`create_transaction_rule`, or `batch_create_rules` for several at once). +7. **Set each series' type** with `set_series_type` — `subscription`, `bill`, `loan`, or `other` — so the Recurring page reads cleanly. +8. **Backfill carefully:** for rules you are confident in, use `apply_rules` to link the matching history into its series. A clean dry run is the prerequisite. +9. **Submit a report** listing each series created (its rule's conditions, typical amount + cadence, type, and match count) and what was backfilled. + +> [!IMPORTANT] +> - Precision over coverage: a wrong rule pulls one-off purchases into a series. When a pattern is uncertain (variable amount, irregular cadence), leave it for a human rather than encoding a fragile rule. +> - Match only on raw, immutable fields (`amount`, `day_of_month`, `month`, `provider_name`, `provider_merchant_name`). Never key a series rule on `series`, `in_series`, `category`, or any mutable display field. +> - Never `apply_rules` on a rule you did not dry-run and judge high-precision. +> - This is the recurring-series counterpart to the Rule Foundation workflow (which owns categories). Stay in your lane: define series, don't re-categorize the backlog.