All work

Native Product Integration

Connecting Finely Transactions to App Intents

App Intents transaction path

Siri, Shortcuts, Spotlight, and payment automation reuse Finely's draft, review, and SwiftData-backed store.

What was at stake

System input must not bypass the rules used by Finely's native transaction form.

Accept conversational or typed input without inventing missing values, bypassing review, or creating a second write path.

The decision

Keep App Intents as adapters to the existing transaction path.

The intent collects input. Finely decides what can be saved. Conversational requests build an editable TransactionDraft; payment automation uses a separate positive-amount path. Both save through TransactionStore.

01System surface02Thin intent03TransactionDraft04Confirm or bound05SwiftData write

Technical deep dive · Transaction boundary

The intent collects input. Finely decides what can be saved.

App Intents collect a phrase or typed parameters. Finely refreshes entitlement state when needed, builds a TransactionDraft, requests confirmation for conversational input, and saves through TransactionStore.shared.

Entry pointsSiri, Shortcuts, Spotlight, payment automation
Adapter scopeThin App Intent perform()
Mutation boundaryTransactionDraft to TransactionStore
PersistenceSwiftData main context

Boundary 01 · Collect without deciding

Treat system parameters as untrusted input.

Shortcuts can pass amount, label, date, category, and account. Siri can provide a short command. TransactionDraft reconciles those explicit fields with parsed values, rejects missing amounts, and discards a category that contradicts the transaction kind.

01 · SystemCollect phrase or fields

Input comes from Siri, Shortcuts, Spotlight, or the payment automation intent.

02 · AdapterNormalize boundary values

Convert money to Decimal and pass values through the intent boundary.

03 · DraftResolve ambiguity

Explicit fields win, contradictions are discarded, and inference stays bounded.

04 · Commit gateConfirm or review

Conversational entry requests confirmation and can open field review. Payment automation uses a separate positive-amount path.

05 · StoreUse the same write path

TransactionStore.shared inserts the model, saves its SwiftData main context, sorts the ledger after success, and posts a change notification.

The intent remains an adapterAddTransactionIntent.swift · pseudo-code
@MainActor
func perform() async throws -> some IntentResult {
  if !Monetization.canAddTransaction() {
    await PurchaseService.shared.refreshEntitlement()
  }

  guard var draft = TransactionDraft(
    message: message,
    amount: amount.map(IntentMoney.decimal),
    category: category,
    account: account,
    history: TransactionStore.shared.transactions
  ) else {
    throw $amount.needsValueError()
  }

  do {
    try await requestConfirmation(dialog: confirmation(for: draft))
  } catch {
    try await review(&draft)
  }

  guard TransactionStore.shared.add(draft.makeTransaction()) else {
    return .result(dialog: notSaved)
  }

  return .result(dialog: added(draft))
}
Invariant

Both transaction intents create a Transaction from a draft and save it through TransactionStore.shared.

A missing amount stops the actionTransactionDraft.swift
guard let amount = explicitAmount ?? parsed.amount,
      amount > 0 else {
  return nil
}

category =
  explicit.category?.side == parsed.kind.side
  ? explicit.category
  : inferCategory()

account = explicit.account
  ?? inferAccount()

One action from system surface to SwiftData

Follow the draft before it is saved.

Siri, Shortcuts, and Spotlight reach AddTransactionIntent. Payment automation reaches LogPaymentIntent. Both paths create a draft and save accepted data through TransactionStore.shared.

System action · ReconstructedTransaction draft
System inputFinely saves
01
Observed in UI · System surface

A user action enters from outside the app.

Siri, Shortcuts, or Spotlight supplies conversational or typed input. LogPaymentIntent receives a positive payment amount and an optional merchant. None of it is persisted yet.

SiriShortcutsWallet
02
Reconstructed from code · Adapter

The App Intent forwards input to the transaction domain.

The intent refreshes cold-launch state, converts boundary values such as money to Decimal, and forwards input to the domain builder.

perform()Decimal
03
App-owned enforcement · Draft

TransactionDraft reconciles the proposed values.

Explicit values are reconciled with parsed values, contradictions are rejected, and inference remains bounded by product policy.

TransactionDraftambiguity visible
04
Commit gate · Review

Conversational entry requests confirmation or field review.

AddTransactionIntent requests confirmation; if it does not complete, the user can edit fields in a review loop. LogPaymentIntent follows its separate positive-amount path without that loop.

confirmationbounded automation
05
App-owned persistence · Store

TransactionStore.shared receives every accepted action.

A successful save appends and sorts the Transaction, then posts the store change notification. A failed save never becomes a success message.

OneTransaction modelSameSwiftData contextCheckedSave result
Evidence boundary

The phone surface is reconstructed. The draft policy, confirmation and review behavior, Decimal conversion, payment path, and TransactionStore write come from the current Finely implementation.

Verification · Draft and store behavior

Test the draft rules and save result.

AppIntentsTests covers parsing, explicit-value precedence, category contradictions, missing amounts, Decimal conversion, and Apple Pay drafts. TransactionStore reports whether the SwiftData save succeeds.

Contradictory categories do not change transaction kindAppIntentsTests.swift · pseudo-code
func testDraftDiscardsContradictoryCategory() {
  let draft = TransactionDraft(
    message: "24 at the bakery",
    amount: nil,
    category: incomeCategory,
    history: []
  )

  XCTAssertEqual(draft?.kind, .expense)
  XCTAssertNotEqual(draft?.category, incomeCategory)
}
01
No missing amount default

A missing amount stops the action; the intent asks for one instead of inventing a value.

02
No parallel persistence path

Every accepted action creates the same Transaction model and saves through TransactionStore.shared.

03
No hidden automation expansion

LogPaymentIntent requires a positive payment amount and keeps merchant handling inside the payment path.

Trade-offs

What this decision adds.

The integration rejects missing amounts and may require field review. It supports fewer shortcuts than a generic parser, but accepted actions follow the same persistence rules as the app.

Outcome and ownership

System actions, one transaction store

Accepted actions write through TransactionStore to the same SwiftData stack, and a failed save is never reported as successful.