Appearance
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
| Layer | Choice | Why |
|---|---|---|
| App | Next.js 16 App Router | Server actions and user interface sit beside the agent surface. |
| Language | TypeScript + Zod | One contract across prompts, application programming interfaces, database payloads, and tests. |
| Database | SQLite/libsql + Drizzle | Fast local demo; Postgres path is a data-source-name/migration swap. |
| Large language model | OpenRouter via Vercel Artificial Intelligence Software Development Kit | One router for model selection; schema-first generateObject. |
| Workflow | Synchronous minimum viable product, Inngest target | Durable execution without rewriting skills. |
| Evals | JavaScript Object Notation Lines harness + Vitest | Golden 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.