Go implementation of Workflow 1 from the Reap CFO Agent take-home.
System design:
Current UI:
Typical flows:
- Manual review denied to escalated queue:
- Manual review resolved and later reflected in completed flow:
- Escalated resolution flow:
- Learning loop refresh:
Confidence examples:
The below examples shows that the system moves to auto approve when the confidence is high enough ( set by the user per tenant).

Temporal visibility:
![]() |
![]() |
| Workflow list view | Workflow execution detail |
The design requires a durable state machine with long-running waits, retries, and human-in-the-loop decisions. Temporal is a better fit than implementing that behavior directly in Postgres.
- Durable workflow execution: state transitions are persisted by Temporal. If a worker crashes during enrichment or tagging, the workflow resumes from the last durable checkpoint instead of rebuilding state from DB locks or cron recovery logic.
- Native human-in-the-loop waits:
IN_REVIEWandESCALATEDboth rely on workflow signals. The workflow can pause safely and resume when the accountant acts, without polling or timeout bookkeeping outside the workflow engine. - Built-in retry model: activity retries are explicit and typed. Network failures, temporary model issues, and strict-prompt retries stay inside workflow orchestration rather than being scattered across handlers or DB jobs.
- Operational visibility: Temporal UI provides execution history, retry visibility, and workflow progression without building a custom state-machine debugger.
The workflow follows the state model from the HLD:
INIT → ENRICHING → TAGGING → AUTO_APPROVE | IN_REVIEW | ESCALATED → SUCCESS
INIT: event accepted and transaction persistedENRICHING: fetch CoA, vendor rules, receipt context, and RAG neighboursTAGGING: LLM or hard rule proposes the CoA mappingAUTO_APPROVE: high-confidence deterministic routeIN_REVIEW: accountant review requiredESCALATED: low-confidence or exception path requiring explicit resolutionSUCCESS: final resolved state after approval, override, or escalated resolution
For local and test runs, the LLM provider is configured through the factory pattern and defaults to a local OpenAI-compatible model endpoint (llm:8080 inside Docker network / mapped host port). This allows end-to-end testing without relying on hosted APIs while preserving the same call contract used by the tagging activity.
Code path:
- provider selection: client.go
- prompt + parse loop: tag.go
- local model wiring: docker-compose.yml
RAG is used as retrieval context for tagging, not as a final decision-maker. PostgreSQL is the primary application store, and pgvector in PostgreSQL is used to store and retrieve historical examples from txn_vectors. The workflow enriches each transaction with nearest historical examples scoped by tenant, then injects that context into the tagging prompt.
- retrieval query is vector similarity search in
pgvector - retrieval is tenant-scoped (
tenant_idisolation) - learning loop writes back confirmed examples into
txn_vectors
Code path:
- retrieve neighbours during enrichment: enrich.go
- embedding + vector query/upsert: postgres.go
- feedback writeback loop: learning_loop.go
Routing is deterministic after tagging. The LLM proposes a code and confidence; the router decides the next path.
>= auto_approve_min (default 0.90) → AUTO_APPROVE → SUCCESS
>= review_min (default 0.65) → IN_REVIEW
< review_min → ESCALATED
Thresholds are stored in the confidence_thresholds table in PostgreSQL and support per-tenant overrides with a global fallback. They are runtime configuration, not hardcoded constants.
Code path:
- runtime fetch: tagging.go
- threshold lookup and update: postgres.go
- API handler: server.go
To update the confidence thresholds dynamically at runtime:
curl -X PUT http://localhost:8080/confidence-thresholds/tenant_1 \
-H "Content-Type: application/json" \
-d '{
"auto_approve_min": 0.95,
"review_min": 0.60
}'The next workflow execution for that tenant will pick up the new thresholds automatically.
Tenant isolation is enforced at the data-access layer.
- CoA lookups are scoped by
tenant_id - vendor rules are scoped by
tenant_id - RAG retrieval is scoped by
tenant_id - worklist and review operations are scoped by
tenant_id
This allows the same merchant to map to different CoA codes for different tenants.
Repeated accountant overrides can be promoted into deterministic vendor rules.
- once the same vendor is corrected to the same code
VendorRulePromotionCounttimes MaybePromoteVendorRulepersists that mapping- future transactions for that vendor bypass the LLM and return with very high confidence
This reduces repeated manual work and pushes stable patterns out of the model path.
The pipeline is designed to fail safely rather than silently.
- LLM output is validated against the tenant's allowed CoA codes
- invalid or incomplete model output triggers a stricter retry path
- repeated failure does not auto-post; it routes to
IN_REVIEW - missing receipt data is surfaced explicitly to the model rather than being silently dropped
cmd/
worker/main.go — Temporal worker (registers workflow + activities)
api/main.go — Gin HTTP API server
internal/
domain/types.go — all types, zero business logic
workflow/tagging.go — Temporal workflow (state machine + signal wait)
activity/
enrich.go — parallel fetch: receipt, vendor, RAG, CoA
tag.go — context assembler + LLM call + output validation
state.go — SetTxnStatus, WriteCorrectionEvent, MaybePromoteVendorRule
learning_loop.go — nightly idempotent RAG refresh
store/postgres.go — pgx + pgvector queries
llm/client.go — Anthropic SDK wrapper
api/server.go — Gin handlers
config/config.go — env config
migrations/
001_initial_schema.sql
- Docker / Docker Compose
- Go 1.22+ if you want to run binaries outside Compose
This implementation follows an ETL-style processing flow: event ingestion, enrichment/transformation, deterministic routing, and review/resolution. The LLM client is wired using the factory pattern, so the design can support different providers such as Anthropic, while the local test setup uses the local model container for end-to-end validation.
- Start all services:
docker compose up -d- Confirm the important endpoints are up:
- API:
http://localhost:8080 - UI:
http://localhost:8080/ui/tenant_1 - Temporal UI:
http://localhost:8081
- Restart application services after code changes:
docker compose rm -sf api worker
docker compose up -d api worker- Tail logs while testing:
docker compose logs -f api workerThe local stack includes:
apionhttp://localhost:8080workertemporalandtemporal-ui- supporting local services configured in Compose
Create a transaction event via the ingestion API. /ingest is the event boundary and should persist the raw transaction before workflow execution.
curl -X POST http://localhost:8080/ingest \
-H "Content-Type: application/json" \
-d '{
"event_id": "evt_001",
"event_type": "transaction.created",
"event_time": "2026-05-11T00:00:00Z",
"transaction": {
"transaction_id": "txn_001",
"tenant_id": "tenant_1",
"source": "reap_card",
"status": "NEW",
"merchant_name": "Amazon Web Services",
"merchant_normalized_name": "amazon web services",
"mcc": "7372",
"mcc_description": "Computer programming / data processing",
"amount": 12.34,
"currency": "USD",
"billing_amount": 12.34,
"billing_currency": "USD",
"fx_rate": 1.0,
"transaction_date": "2026-05-01",
"posted_at": "2026-05-01T10:00:00Z",
"card_last4": "1234",
"description": "AWS monthly subscription",
"receipt_id": "",
"receipt_status": "unmatched"
}
}'Check the unified worklist API:
curl http://localhost:8080/worklist/tenant_1Open the built-in UI:
open http://localhost:8080/ui/tenant_1Trigger the learning loop manually:
curl -X POST http://localhost:8080/learning-loop/tenant_1- Accounting platform sync (Xero/QBO/NetSuite) — noted in HLD as out of scope
- Append-only audit log export — noted in HLD as out of scope
- Real embedding API call —
embedText()in store/postgres.go returns a zero vector; replace with OpenAI text-embedding-3-small or equivalent - Kafka consumer —
POST /ingestis the stand-in; production wires a Sarama consumer that callsclient.ExecuteWorkflow - Receipt OCR pipeline —
FetchReceiptqueries thereceipt_ocrtable; OCR ingestion is a separate service








