All Articles
AI AgentsFeatured

Conversational Banking Rules → Temporal Workflows

How a banking app turns chat into durable Temporal workflows for salary-day rules, crypto-to-fiat conversion, and money moves, and why Query vs Workflow agents beat one mega-agent.

XA
Xenqube AI Research Team
AI Agents & FinTech
July 15, 202616 min read
TemporalAI agentsconversational bankingcrypto fiatworkflow automationMCPfintech

Balance questions are the easy part of consumer banking AI.

The hard request sounds like this: “Whenever my salary hits, convert 20% to USDC, send rent to this account, and park the rest in savings, unless my balance would drop under ₹50,000.”

That utterance is a durable program written in natural language. Stub it with a chatbot plus cron and a delayed salary webhook will double-transfer. Wire it as conversation → structured rule → Temporal workflow → trigger-driven execution, and users will trust the product with payday.

We built that pattern into a banking-style app: crypto↔fiat conversion, money movements, and salary-day rules set entirely by conversation. The lesson was blunt: do not start with twelve specialists. Start with one clear agent, then split into Query vs Workflow the moment you feel the urge to “also book a transfer” inside a balance answer.

Measured product outcomes live in the conversational banking rules case study. This post is the wiring diagram.


What the product actually did

Users talked to the app. The AI did one of two things:

  1. Answered: balances, recent txs, FX quote, “can I afford rent after conversion?”
  2. Programmed: created or edited a durable rule that Temporal owned forever until the user paused it.

Triggers we supported first:

TriggerExample utteranceWorkflow behavior
Salary credit detected“When salary comes on the 1st…”Wait for bank credit webhook / ledger event → run rule
Balance threshold“If USDT > $2,000, convert half to INR”Signal on balance poll / wallet update
Calendar / payday hint“Every month after salary…”Schedule + confirm against actual credit
Manual run“Do the rent split now”One-shot execution of the same activities

Money paths:

  • Fiat ledger moves (internal transfer, payee payout)
  • Crypto → fiat (and reverse) via conversion rails
  • Multi-step recipes (convert → transfer → notify)

Same rails instincts as enterprise settlement: just sitting under a chat UX.


The agent split that saved the build

Trap: one unified mega-agent

One agent with tools for getBalance, createRule, executeTransfer, convertCrypto feels elegant for a week. Then:

  • A simple balance question “helpfully” offers to move money
  • Tool lists bloat; model picks createRule when the user asked “what if”
  • Latency and token cost explode on every read

What worked: Query agent vs Workflow agent

Query agentWorkflow agent
JobRead-only answers + soft simulationsAuthor and mutate durable rules
Toolsbalances, txs, quotes, policy FAQscreate/update/pause rule, attach trigger
Side effectsNone (or draft-only preview)Writes Temporal schedules / workflows
ConfirmationOptionalAlways: show rule card before commit
Model tempLower, citation-friendlyStructured output + Zod

Where the intent router sits, and when you do not need it

This is the confusion: money execution is deterministic. Chat ingress is not.

PathIntent router?Why
Salary webhook / threshold / Schedule → RuleRunWorkflowNoEvent already names the ruleId. Dispatcher is pure code. No LLM.
Platform MCP (create_rule, get_balances, …)NoCaller already chose the tool. AuthZ + DSL validator only.
In-app buttons (“Create salary rule”, “Check balance”)NoUI selected the agent / tool.
Free-text chat (“when salary reaches…”)Yes: thin ingress onlyNatural language is ambiguous until structured.

So: Temporal runs are deterministic DSL interpreters. The intent router is only an NL front-door into Query vs Workflow agents. If your product is mostly forms + MCP + cards, you can skip an LLM router entirely and keep agents only for drafting. If chat is the product, keep the router: but keep it dumb and fail-safe:

  1. Prefer UI mode switch or structured chips when possible.
  2. Else a small classifier (or even keyword + regex + LLM fallback) → query | workflow | clarify.
  3. On low confidence → clarify, never guess create_rule.
  4. On mixed intents (“what’s my balance and also set a rule”) → clarify or run Query first, then offer Workflow card.
  5. Never put the router inside Temporal Activities that move money.

After confirm, the LLM is out of the loop until the user chats again.

That is the same boundary discipline as our banking ops layer and exchange desk copilots: reads vs writes: just productized for consumers. Start unified only for a vertical slice; split Query vs Workflow the day staging shows an accidental side effect.


Conversation → Temporal: the only pipeline that stays honest

                    ┌─ free-text chat ─┐
                    │                  │
                    ▼                  │
              Intent router            │   (NL only: optional if UI/MCP)
              query | workflow | clarify
                 │         │
                 ▼         ▼
           Query Agent   Workflow Agent ──► RuleDraft (Zod) ──► confirm card
                 │                                  │
                 │                                  ▼  user confirms
                 │                         rules DSL (DB) + Temporal register
                 │                                  │
MCP / buttons ───┴────────── (no router) ───────────┘
                                                    │
     salary webhook / threshold ── deterministic dispatcher ──► RuleRunWorkflow
                                                    │
                                          Activities (convert, transfer, notify)
                                                    │
                                          Idempotent ledger + receipt

RuleDraft (conceptually) includes:

  • Trigger type + parameters
  • Ordered steps (convert / transfer / notify)
  • Guards (“abort if post-balance < X”)
  • Limits (max notional / day)
  • Idempotency namespace (userId + ruleId)

Temporal owns:

  • Waiting on salary Signals (or Schedules that verify credit landed)
  • Retries on exchange / bank API failure without double-pay
  • Pause / resume when the user says “stop the salary split”
  • Full history for “why did you send rent twice?” support tickets

The LLM never “remembers” to run next month. The workflow does.


What each rule actually is (DSL + Temporal: not “chat memory”)

Yes: every confirmed rule becomes a durable program, not a prompt the model might forget.

More precisely, each rule is two things:

  1. A typed rule DSL (document): Zod / JSON schema: trigger, steps, guards, caps, ruleId. This is the source of truth stored in your DB (and optionally versioned). Chat and platform MCP only write this after the confirm card.
  2. Temporal durable execution: that DSL is run by Temporal. Do not leave a single Workflow waiting open for three years without a strategy for history limits (below).
PieceRole
rules table (DSL)Canonical definition; pause/edit/version
Trigger pathSalary webhook / threshold / Schedule → dispatcher
RuleRunWorkflow (child, short-lived)One Temporal run per firing: convert → transfer → notify, then complete
Optional RuleSupervisorWorkflowLong-lived waiter / Schedule owner for that ruleId: must Continue-As-New

Avoid: one mega-Workflow that, every payday forever, appends convert/transfer events until history explodes. Prefer short runs per trigger + a thin supervisor/Schedule.

Platform MCP create_rule writes the same DSL and registers the same Temporal wiring: Product B does not invent a second execution engine.


Temporal limits you must design for

Temporal is durable, not infinite-history. Bake these into the banking product:

Limit / concernPractical default (self-hosted & common Cloud guidance)What we do in this product
Event history countHard fail around ~51,200 events (warn earlier, often ~10k)One RuleRunWorkflow per salary/trigger event; complete after steps
Event history sizeHard fail around ~50 MB (warn ~10 MB)Never put statements/PDF/base64 in Activity results: store blob refs, return ids
Continue-As-NewRequired for long-lived waitersSupervisors call Continue-As-New when continueAsNewSuggested (or on a cadence, e.g. monthly)
Pending activities / children / signalsDefaults on the order of thousands pending per executionDon’t pile unbounded Signals; drain / Continue-As-New; use child runs
Activity payloadsInputs/outputs are recorded in historyKeep args tiny (ruleId, eventId, amounts); fetch details inside the Activity
Workflow IdStable id per rule, e.g. rule:{userId}:{ruleId}Supervisor uses fixed Workflow Id; each run gets a new Run Id
IdempotencyYour problem, not Temporal’s aloneActivity id / client key = ruleId:triggerEventId:stepIndex
RetentionNamespace retention eventually closed historiesSupport “why did rent send?” from ledger + short Temporal retention, not endless UI history
Worker concurrency / task queueCluster/CPU boundSeparate queues: rules-exec (money) vs rules-query (reads) so chat load can’t starve payouts
SchedulesFine for calendar hints; still verify real salary creditSchedule may wake a check; execution still keys off credit event id

Quota planning (product, not Temporal hard caps): decide max active rules per user (e.g. 20), max steps per rule (e.g. 8), max notional/day: enforce in the DSL validator before create_rule, not only in the LLM prompt.

If a rule must “live forever,” that means forever as a definition + forever via Continue-As-New / new runs: not forever as one unrekeyed Event History.


Triggers that fire, and how you keep them from firing twice

  1. Banking core / BaaS webhook: salary_credit with amount + account → Signal to waiting SalaryRuleWorkflow.
  2. Ledger projection: if webhook is late, a short poll Activity confirms credit then Signals.
  3. Crypto wallet indexer: balance crossed threshold → Signal ThresholdRuleWorkflow.
  4. Idempotency: Activity client ids = ruleId:triggerEventId:stepIndex. Same salary UTR cannot execute the recipe twice.

If the user’s bank sends two webhooks for one salary, Temporal + idempotency keys make the second a no-op. Chatbots with setInterval do not.


UX that earns the right to move money

Every Workflow agent turn that would persist a rule returns a human-readable card before commit:

When salary credit ≥ ₹X on account ···1234

  1. Convert 20% to USDC (cap $Y/day)
  2. Transfer ₹Z to “Rent” payee
  3. Move remainder to Savings
    Abort if spendable fiat would fall under ₹50,000
    [Confirm] [Edit] [Cancel]

No confirm → no Temporal write. Same maker-checker instinct as institutional banking, compressed into consumer UX.

Query agent can simulate the card without creating the workflow: that alone cut support tickets in half during testing.


Stack notes (practical)

  • Vercel AI SDK (or equivalent) for both agents: Zod tool schemas, streaming chat
  • Temporal for RuleWorkflow + execution Activities
  • MCP in both directions (see next section)
  • Router kept dumb on purpose
  • Optional Judge later: sample failed rules, rewrite clarification prompts: never silent limit changes

For desks and bank ops we deepen the Control plane; for this consumer product the confirm card is the control plane.


Two faces of MCP: adapters and your product as a platform

Most write-ups (including early cuts of ours) only describe southbound MCP: the agent calling tools into the ledger, conversion rail, and payee directory.

That is necessary. It is not enough if you plan a second product.

Northbound / platform MCP is the move: the banking app itself becomes an MCP server that other products (and other agents) can call with the same policy, Temporal durability, and idempotency you already trust.

Future Wealth app / Cards app / HR payroll agent
              │
              │  MCP client (scoped token)
              ▼
┌─────────────────────────────────────┐
│  Banking product: Platform MCP     │
│  get_balances · quote_convert       │
│  create_rule · pause_rule           │
│  preview_salary_recipe              │
└──────────────┬──────────────────────┘
               │ same Activities / Temporal
               ▼
     Ledger · Crypto↔fiat · Payees · Webhooks

Example platform tools you expose (not invent later under a one-off REST mess):

Platform MCP toolWho calls itWhat it must enforce
get_balances / list_payeesWealth AI, support botRead scopes only
quote_convertTax / FX helper productNo side effects
preview_salary_recipeAny UX that drafts rulesDry-run via Query path
create_rule / pause_ruleNew “Goals” product, partner agentsSame confirm / limit / Temporal path as in-app chat
run_rule_nowOps consoleAuthZ + idempotency + audit

Why this matters commercially: when you ship Product #2 (investing, cards, expense, B2B payroll), it does not re-implement salary webhooks and conversion. It connects as an MCP client to the banking product. One Temporal spine. One risk policy. Multiple UIs and agents.

Design rules we deliberately bake in:

  1. Same Activity code for in-app Workflow agent and external MCP callers: never a “fast path” that skips confirms for partners.
  2. Scoped tokens per product (Wealth can preview + create_rule for its users; not global admin).
  3. Southbound stays private: Product B never gets raw core-banking credentials; only platform MCP.
  4. Version the MCP schema: additive tools, deprecate slowly; Temporal workflows outlive any single frontend.

If you only wire southbound MCP (“agent talks to bank APIs”), your next product will either scrape your app or fork the money path. Expose the banking product as MCP from day one of the architecture, even if Product #2 is a year away.


Production hardening (the layer that keeps payday money safe)

The spine above is enough for a credible architecture review. Shipping real salary money needs these controls explicitly in the DSL, Temporal Activities, and ops runbooks: not as “we’ll add later” footnotes.

Partial failure: convert OK, transfer fails

A recipe is not atomic across rails. Design every multi-step rule as a state machine with named compensations:

Step outcomeProduct behavior
Convert succeeds, transfer failsLeave converted balance parked (or auto-revert if policy says so); mark run PARTIAL; pause rule optional; notify user + support code
Transfer 1 OK, transfer 2 failsDo not silently retry step 1; resume from failed step with same idempotency keys
All steps fail before ledger writeMark FAILED; rule stays active unless circuit-breaker trips

Temporal Activities should return structured results ({ status, ledgerTxnId, compensateHint }), not throw opaque errors. Keep a support playbook per PARTIAL code: agents never invent refunds in chat without an Activity.

In-flight pause: user pauses while a run is mid-flight

pause_rule must be a cooperative cancel, not delete-and-pray:

  1. Flip rule status PAUSED in DB (source of truth).
  2. Signal any supervisor / cancel Schedule so new triggers do not start.
  3. In-flight RuleRunWorkflow: honor a cancel Sync / cancellation scope between steps, never mid-Activity if the rail already committed.
  4. If cancel arrives after convert but before transfer, treat as partial failure (above).
  5. UI: “Paused: last run finishing step 2/3” vs “Paused: no active run.”

Step-up auth: biometric / OTP

Enforce outside the LLM:

  • First money-moving rule for a user → step-up (biometric / OTP / bank soft-token).
  • Create / edit when notional or daily cap exceeds threshold → step-up again.
  • Platform MCP create_rule inherits the same step-up requirement via session / assertion token: partners cannot skip it with a service key alone.

Store authAssertionId on the DSL version that was confirmed.

Payee / sanctions checks before new beneficiaries

Before the first transfer step to an unseen payee:

  • Activity screen_payee (sanctions / allowlist / cool-off).
  • Cold payees require confirm card + cooling period if policy demands.
  • Cache screening result with TTL on payeeId; Workflow agent only selects payees: screening is never “model said it looks fine.”

FX / slippage caps on convert steps

Encode in the DSL, enforce in the convert Activity:

convert: { from, to, amountOrPct, maxSlippageBps, quoteId?, abortOnBreach: true }

At execution: re-quote → if slippage vs card/quote exceeds maxSlippageBps, abort that run (rule stays active unless user chose “pause on FX abort”). Never silently fill at any price.

Webhook auth: signed webhooks, replay window

Salary / ledger ingress is a security boundary:

  • Verify HMAC / mTLS signatures.
  • Reject timestamps outside replay window (e.g. ±5 minutes).
  • Deduplicate on provider eventId before starting RuleRunWorkflow.
  • Idempotency keys alone are not enough if you start ten workflows for ten replayed posts.

Observability: failed runs, override rate, queue lag

Ship metrics from day one of money paths:

  • rule_run_total{status=success|partial|failed|aborted_fx|aborted_guard}
  • rule_run_duration_ms
  • Trigger → start lag (webhook received → first Activity)
  • User overrides / edits per 100 runs
  • Pause / cancel during in-flight count

Alert on partial-rate spikes and webhook lag. Chat UX can surface “last run: partial: see details”; dashboards own the ops truth.

DSL versioning: migrate old rules when schema changes

  • Every confirm writes dslVersion + immutable snapshot.
  • Migrations are explicit jobs: v1 → v2 with dry-run report (how many rules need human re-confirm).
  • Breaking changes (new required FX cap) → force re-confirm card, do not silently inject defaults that move money differently.
  • Execution always pins the snapshot version from rule creation/edit, not “latest code’s guess.”

Support replay: “why did rent send?”

Do not send support into the Temporal Web UI as the only answer.

Ship a receipt object per run:

  • ruleId, dslVersion, triggerEventId
  • step timeline with ledger txn ids
  • guard / FX decisions
  • pause/cancel flags

Rebuild the story from ledger + rule snapshot + receipt. Temporal history is for engineers when receipts disagree with reality.


When a unified agent is still OK

  • Prototype week / 51-Hour Sprint with one demo path
  • Query-only banking assistant (no money movement)
  • Internal tools with one privileged operator and hard VPN gates

The moment payday money is real, split Query vs Workflow. On-call gets quieter.


How Xenqube scopes this

We ship this as AI Agent Development for fintech and neo-bank teams: often next to fiat-crypto rails and Xenith Agents patterns. If you have the chat UX but still run rules on crons and hope, Temporal is the upgrade that makes “say it once” true every payday.


Xenqube designs conversational banking systems where Temporal owns the money moves after chat authors the rule.

Read the case study → · Book a Discovery Call → · Banking AI Ops → · 51-Hour Sprint →

XA
Xenqube AI Research Team
AI Agents & FinTech - writing about production AI systems, enterprise architecture, and what actually works in the real world.