Skip to content

Architecture decisions

~6 min read · summary board first, then six ADRs you can drill into

My architecture position in one line: code orchestrates finance workflows; models perform bounded cognition behind schemas; every outcome is logged, replayable, and overridable.

Control plane shape

01FinanceEventCard authorization, bill, payout, foreign exchange, receipt, scheduled pulse.
02Skill registryRoutes only relevant events to the right deterministic or large-language-model-backed skill.
03Bounded cognitionRules first; model only behind Zod schemas or optional narration.
04Decision logOutcome, evidence, versions, idempotency key, override trail.

The six calls that matter

Large language model is a toolClassification, compilation, and narration are bounded subtasks. Workflow state, payment gates, policy verdicts, and rollback are code.Defends auditability and replay.
One outcome contractEvery skill returns autoApply, suggest, refuse, or escalate.Lets the queue, evals, and autonomy ladder stay generic.
Version every decisionPrompt, model, chart of accounts, rules bundle, and idempotency hash live on the row.Answers “why did January differ from March?”
Tenant isolation firstQueries carry tenant scope; retrieval and vendor rules are tenant-partitioned.Cross-tenant leakage is the unacceptable failure mode.
Durable targetThe minimum viable product invokes skills synchronously. Production target is Inngest activities with retries and replay.Swap runner, not cognition.
Overrides are productAn operator correction becomes a vendor rule, retrieval signal, and possible eval case.Human review compounds into autonomy.

In ADR form

Each of the six calls above, written out as an architecture decision record. Click any title to see the full context, decision, consequences, and alternatives considered.

ADR-001 · Large language model is a tool, not the orchestrator Accepted

Context. A finance agent must produce decisions that survive audit. Letting a model orchestrate workflow state means workflow correctness becomes a prompt-engineering problem and replay becomes irreproducible.

Decision. Workflow state, payment gates, policy verdicts, scheduling, and rollback are code. The model is invoked only behind Zod-typed schemas for classification (auto-tagging), optional compilation (natural-language policy → typed rules), and optional narration (the pulse brief).

Consequences. (+) Replay is deterministic for everything except the classifier; the classifier's output is itself logged with prompt + model version. (+) The cognition boundary is one grep — generateStructured calls only. (−) Cannot lean on chain-of-thought to solve workflow ambiguity; ambiguity must become a refusal reason code.

Alternatives considered. Agentic-framework orchestration (LangGraph, CrewAI) — rejected because the orchestrator becomes a black box. Pure-deterministic with no model — rejected because tail-vendor classification has too long a tail for rules alone.

ADR-002 · Four-outcome contract on every skill Accepted

Context. Every reviewer surface (queue, brief, evals, autonomy gate) needs to treat skill outputs uniformly. Per-skill outcome shapes would fragment trust UX and make the autonomy ladder unimplementable.

Decision. Every skill returns one of autoApply | suggest | refuse | escalate. Refusal carries a structured reasonCode from a closed set. Escalate carries a target role.

Consequences. (+) Queue, evals, and autonomy gate are generic and skill-agnostic. (+) Refusal precision and recall become measurable. (−) Skills cannot return novel outcome kinds without a platform change. (−) AP's "money movement requires dual control" had to be expressed via the escalate path, not a new kind.

Alternatives considered. Free-form skill output with per-skill renderers — rejected because the trust surface fragments. Three outcomes without escalate — rejected because AP needs a typed handoff to a role above operator.

ADR-003 · Version every decision, idempotency-hash the version tuple Accepted

Context. "Why did January differ from March?" is the most common audit question in finance ops. Without version stamps and idempotency, that question is unanswerable and replays are unsafe.

Decision. Each decision row carries (promptVersion, modelId, rulesVersion, coaVersion) plus an idempotencyKey that is a hash of the full version tuple plus event identifier. Replays across any of those edits become safe inserts, not duplicates.

Consequences. (+) Re-running an old event against today's settings is a normal operation. (+) Appeals read a stable snapshot. (−) Schema changes to the version tuple require a migration plan; rotating any version forces idempotency recompute.

Alternatives considered. Plain timestamp keys — rejected, doesn't survive replay. Hashing the event alone — rejected, prompt + model changes would silently overwrite.

ADR-004 · Tenant identifier is first-class on every table and query Accepted

Context. Cross-tenant leakage is the unacceptable failure mode. A multi-tenant agent platform that lets one tenant's vendor rules touch another's classifier is unshippable.

Decision. tenantId is a non-optional column on every table that holds tenant data. Every query passes it explicitly; retrieval indexes are tenant-partitioned. A hermetic unit test pins the isolation claim so it is a continuous-integration gate, not a passing-eval observation. Production Postgres adds row-level security on top.

Consequences. (+) Isolation is enforceable at the schema and test layer, not just by convention. (−) No "platform-wide" reads without an explicit scope helper. (−) Adding a new table forces the developer to write the tenant column at creation time.

Alternatives considered. Schema-per-tenant — rejected, doesn't scale operationally. Row-level security only — kept as defense in depth, not the primary boundary.

ADR-005 · Synchronous runtime now, Inngest as the production target Accepted

Context. A real production agent needs durable execution: retries, replay, scheduled work, failure isolation. The MVP timebox does not allow building all of that, but the architecture must not foreclose it.

Decision. Skills run synchronously today. The skill interface (run(event, opts) → SkillRunResult) is shaped as a pure function of event plus tenant context so the runner can be swapped without changing skill code. Inngest is the production target.

Consequences. (+) The swap is a deploy, not a rewrite. (+) Skills are unit-testable as pure functions today. (−) Today's runtime has no retries or backpressure; long-running cognition would block the request thread. (−) Scheduled pulses currently run on cron/CLI rather than a durable scheduler.

Alternatives considered. Inngest from day one — rejected, scope. Temporal — viable, but Inngest's HTTP-native model fits the Vercel-shaped deployment.

ADR-006 · Operator overrides are product, not telemetry Accepted

Context. Reviewer corrections are the highest-quality training signal the system will ever see. Treating them as logs only would waste them.

Decision. An override writes (a) a corrected decision row, (b) a candidate vendor rule (tenant-scoped, append-only via supersession), and (c) a candidate eval case. The corrected coding flows back into the auto-tagging short-circuit so the same surprise does not cost twice.

Consequences. (+) Reviewer work compounds — autonomy graduates from the human's accumulated corrections. (+) The eval harness can replay overrides through today's prompt × model × CoA × vendor_rules and report averted vs still reversed. (−) Vendor-rule promotion needs a confidence threshold to avoid one-off corrections becoming permanent rules.

Alternatives considered. Overrides as comments only — rejected, no learning loop. Auto-promotion to vendor rule on every override — rejected, too aggressive.

Runtime boundaries

Deterministic spinePolicy verdicts, accounts-payable schedules, foreign-exchange estimates, balance checks, dual control, idempotency.
Large-language-model edgeAuto-tagging classification, optional policy compilation, optional pulse narration.
Human gateAutonomy rung decides what can apply; accounts-payable money movement never auto-applies.
Audit trailDecision rows, co-approvals, overrides, vendor-rule supersession, eval replay.

Repo layout

appOperator surfacesDashboard, queue, detail pages, policies, bills, treasury, forecast, agents, brief.
src/skillsAgent cognitionAuto-tagging, policy enforcement, accounts-payable agent, Daily Chief Financial Officer Pulse.
src/libRuntime contractsEvents, outcomes, autonomy, decision log, dual control, formatting.
testsSafety gatesHermetic Vitest cases across contracts, isolation, policy, accounts payable, pulse, and queue behavior.

Tech stack

LayerChoiceWhy
AppNext.js 16 App RouterServer actions and user interface sit beside the agent surface.
LanguageTypeScript + ZodOne contract across prompts, application programming interfaces, database payloads, and tests.
DatabaseSQLite/libsql + DrizzleFast local demo; Postgres path is a data-source-name/migration swap.
Large language modelOpenRouter via Vercel Artificial Intelligence Software Development KitOne router for model selection; schema-first generateObject.
WorkflowSynchronous minimum viable product, Inngest targetDurable execution without rewriting skills.
EvalsJavaScript Object Notation Lines harness + VitestGolden cases, model sweeps, refusal scoring, replay hooks.

Defensible posture

Why I think this holds up in reviewI avoided the “chatbot chief financial officer” trap on purpose. This is a finance control plane: deterministic where money and policy are involved, model-backed only where language or classification earns its keep, and honest about every mocked integration.

Submission pack — Reap Chief Financial Officer Agent take-home