Local-first agent workflow builders for finance promise to keep sensitive transaction data on your device while still enabling LLM-powered automation. The pitch is appealing: build Zapier-style workflows that reconcile expenses, categorize transactions, or generate tax reports without sending account balances to a cloud service. But the execution model gets complicated fast when you need to call external APIs, manage secrets, persist state across crashes, and maintain audit trails.
Ekselio positions itself as “Loveable for finance workflows,” suggesting a visual builder that generates agent code while respecting data residency constraints. The local-first architecture raises immediate plumbing questions: how do you orchestrate tool calls when financial APIs require server-side OAuth, where do you store workflow state without cloud sync, and what prevents an agent from leaking PII when it calls a third-party enrichment service?
Execution Topology
A local-first finance agent runs in one of three places:
- Browser runtime: JavaScript execution in the client, limited to CORS-friendly APIs and browser storage
- Desktop process: Electron or Tauri shell with filesystem access, native crypto libraries, and unrestricted network calls
- Edge worker: Cloudflare Worker or Deno Deploy instance that runs close to the user but still requires network round-trips
The choice determines your tool execution boundary. Browser runtimes cannot safely store API keys or OAuth refresh tokens. Desktop processes can encrypt secrets with OS keychains but complicate distribution. Edge workers solve the secret storage problem but reintroduce cloud dependencies.
Most local-first builders choose the desktop process model. You ship a binary that embeds a workflow engine, a local SQLite database for state, and a secret vault backed by the OS credential manager. The agent runs entirely on the user’s machine. External API calls happen directly from the desktop process, not through a proxy server.
Tool Call Orchestration
Financial workflows typically need tools that fall into three categories:
| Tool Type | Example | Execution Constraint |
|---|---|---|
| Read-only data fetch | Plaid account balance | Requires OAuth token, safe to cache |
| Mutation with side effects | Stripe payment creation | Requires idempotency key, must log to audit trail |
| Third-party enrichment | Merchant categorization API | May leak transaction details, needs PII filtering |
The orchestration layer must handle each differently. Read-only tools can cache responses in local SQLite to avoid redundant API calls. Mutation tools need idempotency tracking so a workflow retry doesn’t double-charge a customer. Enrichment tools require a data sanitization step before the external call.
Here’s a simplified tool execution wrapper that enforces these constraints:
interface ToolCall {
name: string;
params: Record<string, unknown>;
category: 'read' | 'mutate' | 'enrich';
}
async function executeTool(call: ToolCall, context: WorkflowContext): Promise<unknown> {
// Check cache for read-only tools
if (call.category === 'read') {
const cached = await context.db.get('tool_cache', call.name, call.params);
if (cached && !cached.expired) return cached.result;
}
// Generate idempotency key for mutations
const idempotencyKey = call.category === 'mutate'
? await context.db.getOrCreateIdempotencyKey(call.name, call.params)
: null;
// Sanitize params for enrichment calls
const sanitizedParams = call.category === 'enrich'
? await sanitizePII(call.params, context.piiRules)
: call.params;
// Execute with secret injection
const result = await context.toolRegistry.call(call.name, {
...sanitizedParams,
_idempotencyKey: idempotencyKey,
_secrets: await context.vault.getSecretsFor(call.name)
});
// Log to audit trail for mutations
if (call.category === 'mutate') {
await context.audit.log({
tool: call.name,
params: call.params,
result: result,
timestamp: Date.now()
});
}
return result;
}
The key insight: tool execution is not a simple function call. You need a context object that carries the local database handle, the secret vault, the audit logger, and the PII sanitization rules. Each tool category gets a different execution path.
State Persistence Without Cloud Sync
Finance workflows often run for hours or days. A tax report generator might fetch transactions from multiple accounts, apply categorization rules, calculate deductions, and produce a PDF. If the workflow crashes halfway through, you need to resume without re-fetching all the data or re-running expensive LLM calls.
Local-first builders typically use a checkpoint-based state machine:
- Workflow definition: A DAG of steps with input/output schemas
- Execution state: A SQLite table tracking which steps completed and their outputs
- Checkpoint triggers: After each step completes, write its output to the state table
- Resume logic: On restart, skip completed steps and resume from the last checkpoint
The state table schema looks like this:
CREATE TABLE workflow_executions (
id TEXT PRIMARY KEY,
workflow_name TEXT NOT NULL,
started_at INTEGER NOT NULL,
completed_at INTEGER,
status TEXT CHECK(status IN ('running', 'completed', 'failed'))
);
CREATE TABLE step_executions (
execution_id TEXT NOT NULL,
step_name TEXT NOT NULL,
started_at INTEGER NOT NULL,
completed_at INTEGER,
input_hash TEXT NOT NULL,
output BLOB,
error TEXT,
PRIMARY KEY (execution_id, step_name),
FOREIGN KEY (execution_id) REFERENCES workflow_executions(id)
);
The input_hash column is critical. It lets you detect when a step’s inputs changed, invalidating the cached output. If a user edits the workflow definition or updates a parameter, the hash changes and the step re-runs.
Secret Management and OAuth Flows
Financial APIs require OAuth tokens that expire and refresh. A local-first agent cannot store these in plaintext SQLite. The standard solution is OS-level credential storage:
- macOS: Keychain Services API
- Windows: Credential Manager (CredWrite/CredRead)
- Linux: libsecret with GNOME Keyring or KWallet backend
You store the OAuth refresh token in the OS vault and the access token in memory. When the access token expires, the agent uses the refresh token to get a new one without user interaction.
The tricky part is the initial OAuth flow. Most financial APIs use the authorization code grant, which requires a redirect URI. A desktop app cannot host a public HTTPS endpoint. The workaround is a local loopback server:
- Agent starts a temporary HTTP server on
http://localhost:random-port - Opens the OAuth provider’s authorization URL in the default browser
- User approves the request
- Provider redirects to
http://localhost:random-port?code=... - Agent exchanges the code for tokens and shuts down the server
This works but creates a poor user experience. Some builders use a cloud-hosted OAuth proxy that stores only the refresh token and returns it to the desktop app via a one-time code. This reintroduces a cloud dependency but avoids storing long-lived tokens on a server.
Version Control for Workflow Definitions
Finance workflows often embed proprietary logic: custom categorization rules, tax calculation formulas, or fraud detection heuristics. You cannot commit these to a public Git repo. But you still need version control for collaboration and rollback.
Local-first builders solve this with encrypted workflow bundles:
- Workflow definition is a JSON or YAML file
- Sensitive fields (API keys, calculation formulas) are encrypted with a team key
- The bundle is committed to Git with encrypted blobs
- Developers decrypt locally using a key stored in their OS vault
The encryption happens at the field level, not the file level. This lets you diff workflow changes in Git without exposing secrets. A typical encrypted field looks like:
steps:
- name: calculate_tax
tool: custom_formula
params:
formula: "ENC[AES256:base64-ciphertext]"
rate: 0.21 # plaintext, not sensitive
The builder UI decrypts the formula when rendering the workflow editor and re-encrypts on save.
Data Sanitization for Third-Party Tools
Merchant categorization APIs are useful but risky. You send a transaction description like “WHOLE FOODS MARKET #12345” and get back a category like “Groceries.” The API provider now knows where you shop.
A local-first agent should sanitize transaction details before sending them to third-party enrichment services:
- Strip merchant IDs and location codes
- Normalize amounts to ranges (e.g., “$47.23” becomes “$40-50”)
- Hash account numbers and replace with stable pseudonyms
- Remove timestamps or round to the nearest day
The sanitization rules are part of the workflow definition. Each third-party tool declares what fields it needs, and the agent applies the minimum necessary transformation.
Audit Trail Requirements
Financial workflows must log every action for compliance and debugging. The audit trail needs:
- Immutability: Append-only log that cannot be edited after writing
- Completeness: Every tool call, every decision, every error
- Queryability: Fast lookups by date range, workflow ID, or tool name
SQLite with a write-ahead log (WAL) provides immutability and queryability. You create an append-only table with a trigger that prevents updates and deletes:
CREATE TABLE audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp INTEGER NOT NULL,
execution_id TEXT NOT NULL,
step_name TEXT NOT NULL,
tool_name TEXT,
action TEXT NOT NULL,
details TEXT,
CHECK (id > 0) -- Prevents manual ID assignment
);
CREATE TRIGGER prevent_audit_modifications
BEFORE UPDATE ON audit_log
BEGIN
SELECT RAISE(ABORT, 'Audit log is immutable');
END;
CREATE TRIGGER prevent_audit_deletions
BEFORE DELETE ON audit_log
BEGIN
SELECT RAISE(ABORT, 'Audit log is immutable');
END;
The details column stores a JSON blob with tool inputs, outputs, and errors. You can query it with SQLite’s JSON functions for debugging.
Failure Modes
Local-first finance agents fail in predictable ways:
| Failure | Symptom | Mitigation |
|---|---|---|
| OAuth token expires during long workflow | API calls fail mid-execution | Store refresh token, retry with fresh access token |
| User closes app during mutation step | Partial state, duplicate charges | Use idempotency keys, checkpoint before mutations |
| Third-party API changes response schema | Tool execution throws parse error | Version tool schemas, validate responses before caching |
| SQLite database corrupts | Workflow state lost | Enable WAL mode, periodic backups to user-controlled storage |
The most insidious failure is silent data leakage. If a workflow calls an enrichment API without sanitizing inputs, you’ve violated data residency guarantees. The mitigation is mandatory sanitization hooks in the tool execution layer.
Technical Verdict
Use local-first finance agent builders when:
- You need to process sensitive financial data without cloud storage
- Workflows are user-specific and do not require team collaboration
- You can distribute a desktop app and manage OS-specific secret storage
- Audit requirements allow local-only logs with user-controlled backups
Avoid when:
- Workflows need real-time collaboration or centralized policy enforcement
- You rely on cloud-only APIs that block requests from residential IPs
- Users expect mobile access or cross-device sync
- Compliance requires server-side audit logs with tamper-proof timestamps
The local-first model works well for personal finance automation and small business bookkeeping. It breaks down when you need centralized control, real-time collaboration, or integration with cloud-native financial platforms that expect server-to-server authentication.